From 5d77fb414b3c55b2e236ef612fa5c504fda58ca7 Mon Sep 17 00:00:00 2001 From: Sion Kang Date: Fri, 3 Jul 2026 05:42:38 +0900 Subject: [PATCH] chore(deps): drop bssh-russh fork, use upstream russh 0.62.1 Both reasons the fork existed are now upstreamed: - high-frequency PTY output fix (Handle::data from spawned tasks): upstream #731, released in russh 0.62.0 - SHA-1 MAC exclusion from Preferred::DEFAULT: upstream #690, released in russh 0.60.2 So crates/bssh-russh is no longer needed. Switch the dependency to the crates.io russh 0.62.1 and remove the vendored fork (66 files). - Cargo.toml: russh = "0.62.1" (was package = bssh-russh, path fork) - ssh-key pin bumped =0.7.0-rc.10 -> =0.7.0-rc.11 to match russh 0.62.x - handler: adapt channel_open_session to the new russh 0.62 signature (ChannelOpenHandle reply + Result<()> instead of Result); accept the channel via reply.accept() - audit.toml: reword the RUSTSEC-2023-0071 note (no more vendored fork) Verified: cargo build, clippy, fmt clean, and full test suite green (1233 lib tests, PTY stress 10/10, streaming high-throughput 28/28). --- .cargo/audit.toml | 4 +- Cargo.lock | 418 ++- Cargo.toml | 13 +- crates/bssh-russh/Cargo.toml | 148 - crates/bssh-russh/README.md | 41 - crates/bssh-russh/create-patch.sh | 65 - .../bssh-russh/patches/handle-data-fix.patch | 148 - crates/bssh-russh/src/auth.rs | 313 --- crates/bssh-russh/src/cert.rs | 46 - crates/bssh-russh/src/channels/channel_ref.rs | 33 - .../bssh-russh/src/channels/channel_stream.rs | 63 - crates/bssh-russh/src/channels/io/mod.rs | 44 - crates/bssh-russh/src/channels/io/rx.rs | 85 - crates/bssh-russh/src/channels/io/tx.rs | 202 -- crates/bssh-russh/src/channels/mod.rs | 843 ------ crates/bssh-russh/src/cipher/benchmark.rs | 48 - crates/bssh-russh/src/cipher/block.rs | 409 --- crates/bssh-russh/src/cipher/cbc.rs | 137 - .../bssh-russh/src/cipher/chacha20poly1305.rs | 143 - crates/bssh-russh/src/cipher/clear.rs | 102 - crates/bssh-russh/src/cipher/gcm.rs | 190 -- crates/bssh-russh/src/cipher/mod.rs | 371 --- crates/bssh-russh/src/client/encrypted.rs | 1124 -------- crates/bssh-russh/src/client/kex.rs | 400 --- crates/bssh-russh/src/client/mod.rs | 2428 ----------------- crates/bssh-russh/src/client/session.rs | 541 ---- crates/bssh-russh/src/client/test.rs | 161 -- crates/bssh-russh/src/compression.rs | 355 --- crates/bssh-russh/src/helpers.rs | 231 -- crates/bssh-russh/src/kex/curve25519.rs | 175 -- crates/bssh-russh/src/kex/dh/groups.rs | 319 --- crates/bssh-russh/src/kex/dh/mod.rs | 354 --- crates/bssh-russh/src/kex/ecdh_nistp.rs | 246 -- crates/bssh-russh/src/kex/hybrid_mlkem.rs | 434 --- crates/bssh-russh/src/kex/mod.rs | 490 ---- crates/bssh-russh/src/kex/none.rs | 73 - crates/bssh-russh/src/keys/agent/client.rs | 624 ----- crates/bssh-russh/src/keys/agent/mod.rs | 193 -- crates/bssh-russh/src/keys/agent/msg.rs | 23 - crates/bssh-russh/src/keys/agent/server.rs | 403 --- crates/bssh-russh/src/keys/format/mod.rs | 152 -- crates/bssh-russh/src/keys/format/openssh.rs | 17 - crates/bssh-russh/src/keys/format/pkcs5.rs | 46 - crates/bssh-russh/src/keys/format/pkcs8.rs | 176 -- .../src/keys/format/pkcs8_legacy.rs | 223 -- crates/bssh-russh/src/keys/format/tests.rs | 26 - crates/bssh-russh/src/keys/key.rs | 125 - crates/bssh-russh/src/keys/known_hosts.rs | 232 -- crates/bssh-russh/src/keys/mod.rs | 1566 ----------- crates/bssh-russh/src/lib.rs | 96 - crates/bssh-russh/src/lib_inner.rs | 503 ---- crates/bssh-russh/src/mac/crypto.rs | 63 - crates/bssh-russh/src/mac/crypto_etm.rs | 57 - crates/bssh-russh/src/mac/mod.rs | 123 - crates/bssh-russh/src/mac/none.rs | 26 - crates/bssh-russh/src/msg.rs | 163 -- crates/bssh-russh/src/negotiation.rs | 515 ---- crates/bssh-russh/src/parsing.rs | 272 -- crates/bssh-russh/src/pty.rs | 134 - crates/bssh-russh/src/server/encrypted.rs | 1526 ----------- crates/bssh-russh/src/server/kex.rs | 399 --- crates/bssh-russh/src/server/mod.rs | 1170 -------- crates/bssh-russh/src/server/session.rs | 1570 ----------- crates/bssh-russh/src/session.rs | 1468 ---------- crates/bssh-russh/src/ssh_read.rs | 363 --- crates/bssh-russh/src/sshbuffer.rs | 574 ---- crates/bssh-russh/src/tests.rs | 1408 ---------- crates/bssh-russh/sync-upstream.sh | 156 -- crates/bssh-russh/tests/pty_handle_data.rs | 204 -- src/server/handler.rs | 10 +- 70 files changed, 176 insertions(+), 25627 deletions(-) delete mode 100644 crates/bssh-russh/Cargo.toml delete mode 100644 crates/bssh-russh/README.md delete mode 100755 crates/bssh-russh/create-patch.sh delete mode 100644 crates/bssh-russh/patches/handle-data-fix.patch delete mode 100644 crates/bssh-russh/src/auth.rs delete mode 100644 crates/bssh-russh/src/cert.rs delete mode 100644 crates/bssh-russh/src/channels/channel_ref.rs delete mode 100644 crates/bssh-russh/src/channels/channel_stream.rs delete mode 100644 crates/bssh-russh/src/channels/io/mod.rs delete mode 100644 crates/bssh-russh/src/channels/io/rx.rs delete mode 100644 crates/bssh-russh/src/channels/io/tx.rs delete mode 100644 crates/bssh-russh/src/channels/mod.rs delete mode 100644 crates/bssh-russh/src/cipher/benchmark.rs delete mode 100644 crates/bssh-russh/src/cipher/block.rs delete mode 100644 crates/bssh-russh/src/cipher/cbc.rs delete mode 100644 crates/bssh-russh/src/cipher/chacha20poly1305.rs delete mode 100644 crates/bssh-russh/src/cipher/clear.rs delete mode 100644 crates/bssh-russh/src/cipher/gcm.rs delete mode 100644 crates/bssh-russh/src/cipher/mod.rs delete mode 100644 crates/bssh-russh/src/client/encrypted.rs delete mode 100644 crates/bssh-russh/src/client/kex.rs delete mode 100644 crates/bssh-russh/src/client/mod.rs delete mode 100644 crates/bssh-russh/src/client/session.rs delete mode 100644 crates/bssh-russh/src/client/test.rs delete mode 100644 crates/bssh-russh/src/compression.rs delete mode 100644 crates/bssh-russh/src/helpers.rs delete mode 100644 crates/bssh-russh/src/kex/curve25519.rs delete mode 100644 crates/bssh-russh/src/kex/dh/groups.rs delete mode 100644 crates/bssh-russh/src/kex/dh/mod.rs delete mode 100644 crates/bssh-russh/src/kex/ecdh_nistp.rs delete mode 100644 crates/bssh-russh/src/kex/hybrid_mlkem.rs delete mode 100644 crates/bssh-russh/src/kex/mod.rs delete mode 100644 crates/bssh-russh/src/kex/none.rs delete mode 100644 crates/bssh-russh/src/keys/agent/client.rs delete mode 100644 crates/bssh-russh/src/keys/agent/mod.rs delete mode 100644 crates/bssh-russh/src/keys/agent/msg.rs delete mode 100644 crates/bssh-russh/src/keys/agent/server.rs delete mode 100644 crates/bssh-russh/src/keys/format/mod.rs delete mode 100644 crates/bssh-russh/src/keys/format/openssh.rs delete mode 100644 crates/bssh-russh/src/keys/format/pkcs5.rs delete mode 100644 crates/bssh-russh/src/keys/format/pkcs8.rs delete mode 100644 crates/bssh-russh/src/keys/format/pkcs8_legacy.rs delete mode 100644 crates/bssh-russh/src/keys/format/tests.rs delete mode 100644 crates/bssh-russh/src/keys/key.rs delete mode 100644 crates/bssh-russh/src/keys/known_hosts.rs delete mode 100644 crates/bssh-russh/src/keys/mod.rs delete mode 100644 crates/bssh-russh/src/lib.rs delete mode 100644 crates/bssh-russh/src/lib_inner.rs delete mode 100644 crates/bssh-russh/src/mac/crypto.rs delete mode 100644 crates/bssh-russh/src/mac/crypto_etm.rs delete mode 100644 crates/bssh-russh/src/mac/mod.rs delete mode 100644 crates/bssh-russh/src/mac/none.rs delete mode 100644 crates/bssh-russh/src/msg.rs delete mode 100644 crates/bssh-russh/src/negotiation.rs delete mode 100644 crates/bssh-russh/src/parsing.rs delete mode 100755 crates/bssh-russh/src/pty.rs delete mode 100644 crates/bssh-russh/src/server/encrypted.rs delete mode 100644 crates/bssh-russh/src/server/kex.rs delete mode 100644 crates/bssh-russh/src/server/mod.rs delete mode 100644 crates/bssh-russh/src/server/session.rs delete mode 100644 crates/bssh-russh/src/session.rs delete mode 100644 crates/bssh-russh/src/ssh_read.rs delete mode 100644 crates/bssh-russh/src/sshbuffer.rs delete mode 100644 crates/bssh-russh/src/tests.rs delete mode 100755 crates/bssh-russh/sync-upstream.sh delete mode 100644 crates/bssh-russh/tests/pty_handle_data.rs diff --git a/.cargo/audit.toml b/.cargo/audit.toml index d0c01ba1..1136defd 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -8,8 +8,8 @@ ignore = [ # # No fixed upgrade is available for any version of the `rsa` crate. The vulnerability # is inherent to the current RustCrypto RSA implementation, which lacks constant-time - # primitives. Both `rsa 0.9.x` (via ssh-key 0.6.x) and `rsa 0.10.0-rc.x` (via the - # vendored bssh-russh fork) are affected. + # primitives. Both `rsa 0.9.x` (via ssh-key 0.6.x) and `rsa 0.10.0-rc.x` (via + # russh's ssh-key) are affected. # # Mitigation: users should prefer Ed25519 or ECDSA host/user keys over RSA where # possible. The Marvin Attack requires precise timing measurements over many diff --git a/Cargo.lock b/Cargo.lock index cfbcaf23..5124f5a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,12 +10,12 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" -version = "0.6.0-rc.10" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ "crypto-common 0.2.2", - "inout 0.2.2", + "inout", ] [[package]] @@ -32,9 +32,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0-rc.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ "aead", "aes", @@ -350,15 +350,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array 0.14.7", -] - [[package]] name = "block-padding" version = "0.4.2" @@ -387,7 +378,6 @@ dependencies = [ "async-compression", "async-trait", "bcrypt", - "bssh-russh", "bssh-russh-sftp", "bytes", "chrono", @@ -413,6 +403,7 @@ dependencies = [ "ratatui", "regex", "rpassword", + "russh", "rustls-native-certs", "rustyline", "secrecy", @@ -442,83 +433,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bssh-russh" -version = "0.61.1" -dependencies = [ - "aes", - "async-trait", - "aws-lc-rs", - "bitflags 2.11.1", - "block-padding 0.4.2", - "byteorder", - "bytes", - "cbc", - "cipher", - "crypto-bigint", - "ctr", - "curve25519-dalek", - "data-encoding", - "delegate", - "der", - "des", - "digest 0.11.3", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", - "enum_dispatch", - "env_logger", - "flate2", - "futures", - "generic-array 1.4.1", - "getrandom 0.2.17", - "ghash", - "hex-literal", - "hkdf", - "hmac", - "inout 0.1.4", - "internal-russh-num-bigint", - "keccak", - "log", - "md5", - "ml-kem", - "module-lattice", - "num-bigint", - "p256", - "p384", - "p521", - "pageant", - "pbkdf2", - "pkcs1", - "pkcs5", - "pkcs8", - "polyval", - "rand 0.10.1", - "rand_core 0.10.1", - "ring", - "rsa", - "russh-cryptovec", - "russh-util", - "salsa20", - "scrypt", - "sec1", - "sha1", - "sha2 0.11.0", - "sha3", - "signature", - "spki", - "ssh-encoding", - "ssh-key", - "subtle", - "tempfile", - "thiserror 2.0.18", - "tokio", - "typenum", - "universal-hash", - "yasna", - "zeroize", -] - [[package]] name = "bssh-russh-sftp" version = "2.3.0" @@ -673,7 +587,7 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.2", - "inout 0.2.2", + "inout", "zeroize", ] @@ -958,9 +872,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ "cpubits", "ctutils", @@ -1036,12 +950,12 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "5.0.0-pre.6" +version = "5.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" +checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", @@ -1254,22 +1168,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" -[[package]] -name = "dsa" -version = "0.7.0-rc.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a914d1d3e30ea021331fc4c4b3558aba5b1a26d91cddb920322d209a3a43cb99" -dependencies = [ - "crypto-bigint", - "crypto-primes", - "der", - "digest 0.11.3", - "rfc6979", - "sha2 0.11.0", - "signature", - "zeroize", -] - [[package]] name = "dunce" version = "1.0.5" @@ -1278,9 +1176,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ecdsa" -version = "0.17.0-rc.18" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der", "digest 0.11.3", @@ -1303,9 +1201,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-pre.7" +version = "3.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6" +checksum = "1685663e23882cd8517dcbcb1c23a6ebff4433c22dfb681d760219b62cd1b849" dependencies = [ "curve25519-dalek", "ed25519", @@ -1325,22 +1223,21 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" -version = "0.14.0-rc.32" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda94f31325c4275e9706adecbb6f0650dee2f904c915a98e3d81adaaaa757aa" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", "crypto-common 0.2.2", "digest 0.11.3", + "ff", + "group", "hkdf", "hybrid-array", - "once_cell", "pem-rfc7468", "pkcs8", "rand_core 0.10.1", - "rustcrypto-ff", - "rustcrypto-group", "sec1", "subtle", "zeroize", @@ -1370,29 +1267,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1440,6 +1314,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1643,10 +1527,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1668,11 +1550,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -1702,6 +1586,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "h2" version = "0.4.14" @@ -1861,9 +1756,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "ctutils", "subtle", @@ -2101,23 +1996,13 @@ dependencies = [ "rustversion", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding 0.3.3", - "generic-array 0.14.7", -] - [[package]] name = "inout" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "block-padding 0.4.2", + "block-padding", "hybrid-array", ] @@ -2200,30 +2085,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6835eea34fb6321b9b3aa7b685c2b433948c09447e389dc017fdf687d5d11e65" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c22e04db9c58f5136eb1757f3d5c49a7b187f49e52185228cbd2f5acdfcc08c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -2397,9 +2258,9 @@ dependencies = [ [[package]] name = "md5" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" [[package]] name = "memchr" @@ -2775,9 +2636,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "p256" -version = "0.14.0-rc.9" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b97e3bf0465157ae90975ff52dbeb1362ba618924878c9f74c25baa27a65f9a" +checksum = "b6bb40a5099e2c38a09dd29321a7a7f045f165a54317679c7cdfb0cbaf8f6b1e" dependencies = [ "ecdsa", "elliptic-curve", @@ -2788,9 +2649,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.14.0-rc.9" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437f30ebcb1e16ff48acead5f08bd69fbcdbc82421687bb48af5c315a0bfab03" +checksum = "492f329d7eb11d22dadc988626b9ea1f503b4ab043a8b1e4e2cc4ae45dabdd70" dependencies = [ "ecdsa", "elliptic-curve", @@ -2802,9 +2663,9 @@ dependencies = [ [[package]] name = "p521" -version = "0.14.0-rc.9" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9fd792bab86ecf6249561752fb5a413511f999887107dd054bbda5143743d7" +checksum = "ff42e4ace5424e3b6d7cb82514be89866b85af87015f80341e37dcc21a66ce6e" dependencies = [ "base16ct", "ecdsa", @@ -3137,15 +2998,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -3208,25 +3060,29 @@ dependencies = [ [[package]] name = "primefield" -version = "0.14.0-rc.9" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ "crypto-bigint", "crypto-common 0.2.2", + "ff", "rand_core 0.10.1", - "rustcrypto-ff", "subtle", "zeroize", ] [[package]] name = "primeorder" -version = "0.14.0-rc.9" +version = "0.14.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a" +checksum = "478aa2e499c7164b21b4c5e8610018e5b83a284da5f9e0896907dd4749cd76c9" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -3552,12 +3408,12 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ + "crypto-bigint", "hmac", - "subtle", ] [[package]] @@ -3614,11 +3470,83 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "russh" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d2039c3e50abb1ec3bdadea27098679da12a33befa9ab4e62adad9a13ebcb2" +dependencies = [ + "aes", + "aws-lc-rs", + "bitflags 2.11.1", + "block-padding", + "byteorder", + "bytes", + "cbc", + "cipher", + "crypto-bigint", + "ctr", + "curve25519-dalek", + "data-encoding", + "delegate", + "der", + "digest 0.11.3", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "flate2", + "futures", + "generic-array 1.4.1", + "getrandom 0.4.2", + "ghash", + "hex-literal", + "hmac", + "inout", + "internal-russh-num-bigint", + "keccak", + "log", + "md5", + "ml-kem", + "module-lattice", + "num-bigint", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2", + "pkcs1", + "pkcs5", + "pkcs8", + "polyval", + "rand 0.10.1", + "rand_core 0.10.1", + "rsa", + "russh-cryptovec", + "russh-util", + "salsa20", + "scrypt", + "sec1", + "sha1", + "sha2 0.11.0", + "sha3", + "signature", + "spki", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror 2.0.18", + "tokio", + "typenum", + "universal-hash", + "zeroize", +] + [[package]] name = "russh-cryptovec" -version = "0.61.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51" +checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" dependencies = [ "log", "nix 0.31.3", @@ -3647,27 +3575,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustcrypto-ff" -version = "0.14.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" -dependencies = [ - "rand_core 0.10.1", - "subtle", -] - -[[package]] -name = "rustcrypto-group" -version = "0.14.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" -dependencies = [ - "rand_core 0.10.1", - "rustcrypto-ff", - "subtle", -] - [[package]] name = "rustix" version = "1.1.4" @@ -4157,17 +4064,15 @@ dependencies = [ [[package]] name = "ssh-cipher" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" dependencies = [ "aead", "aes", "aes-gcm", - "cbc", "chacha20", "cipher", - "ctr", "ctutils", "des", "poly1305", @@ -4177,9 +4082,9 @@ dependencies = [ [[package]] name = "ssh-encoding" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" dependencies = [ "base64ct", "bytes", @@ -4192,14 +4097,13 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.7.0-rc.10" +version = "0.7.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b" +checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" dependencies = [ "argon2 0.6.0-rc.8", "bcrypt-pbkdf", "ctutils", - "dsa", "ed25519-dalek", "hex", "hmac", @@ -4209,7 +4113,6 @@ dependencies = [ "rand_core 0.10.1", "rsa", "sec1", - "serde", "sha1", "sha2 0.11.0", "signature", @@ -5445,20 +5348,21 @@ dependencies = [ ] [[package]] -name = "writeable" -version = "0.6.3" +name = "wnaf" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] [[package]] -name = "yasna" -version = "0.5.2" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "bit-vec", - "num-bigint", -] +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" @@ -5526,18 +5430,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index acb82cfd..79e75e73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ ".", - "crates/bssh-russh", "crates/bssh-russh-sftp", ] @@ -25,10 +24,10 @@ rust-version = "1.93" [dependencies] bytes = "1.11.1" tokio = { version = "1.52.3", features = ["full"] } -# Use our internal russh fork with session loop fixes -# - Development: uses local path (crates/bssh-russh) -# - Publishing: uses crates.io version (path ignored) -russh = { package = "bssh-russh", version = "0.61.1", path = "crates/bssh-russh" } +# 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" # 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,9 +68,9 @@ ipnetwork = "0.21" bcrypt = "0.19" argon2 = { version = "0.5", features = ["std"] } rand = "0.10" -# Pinned to match russh's ssh-key (0.61.x uses =0.7.0-rc.10) so the workspace +# Pinned to match russh's ssh-key (0.62.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.10", features = ["std"] } +ssh-key = { version = "=0.7.0-rc.11", features = ["std"] } async-compression = { version = "0.4", features = ["tokio", "gzip"] } serde_json = "1.0" opentelemetry = "0.32" diff --git a/crates/bssh-russh/Cargo.toml b/crates/bssh-russh/Cargo.toml deleted file mode 100644 index efba0c1e..00000000 --- a/crates/bssh-russh/Cargo.toml +++ /dev/null @@ -1,148 +0,0 @@ -[package] -name = "bssh-russh" -version = "0.61.1" -authors = ["Jeongkyu Shin "] -description = "Temporary fork of russh 0.61.1 with high-frequency PTY output fix (Handle::data from spawned tasks)" -documentation = "https://docs.rs/bssh-russh" -edition = "2024" -homepage = "https://github.com/lablup/bssh" -keywords = ["ssh"] -license = "Apache-2.0" -readme = "README.md" -repository = "https://github.com/lablup/bssh" -rust-version = "1.93" - -# Internal lib name matches upstream (`russh`) so vendored source and the -# regression test in tests/ reference it as `russh::...`; the crate is still -# consumed as `bssh-russh` via the package alias in the workspace root manifest. -# doctest = false: the vendored upstream doctests need russh's repo test fixtures -# and extra dev-deps we don't carry (src/ only), so we skip them — the 143 unit -# tests + tests/pty_handle_data.rs cover this fork. -[lib] -name = "russh" -doctest = false - -# This crate vendors upstream russh source verbatim (+ the PTY fix). Don't hold -# that code to bssh's workspace `-D warnings` for purely stylistic clippy lints -# upstream itself doesn't fix. `[lints]` lives in Cargo.toml, which -# ./sync-upstream.sh preserves, so this survives future syncs. -[lints.clippy] -manual_saturating_arithmetic = "allow" -# collapsible_if fires across upstream's nested `if`s because rust-version >= 1.88 -# makes clippy treat let-chains as stable; upstream russh has not collapsed them. -collapsible_if = "allow" - -[features] -default = ["flate2", "aws-lc-rs", "rsa"] -_bench = [] # Internal benchmark feature -aws-lc-rs = ["dep:aws-lc-rs"] -async-trait = ["dep:async-trait"] -legacy-ed25519-pkcs8-parser = ["yasna"] -des = ["dep:des"] -dsa = ["ssh-key/dsa"] -ring = ["dep:ring"] -rsa = ["dep:rsa", "dep:pkcs1", "ssh-key/rsa"] -serde = ["ssh-key/serde"] - -# Dependency versions mirror upstream russh 0.61.1 (warp-tech/russh, tag v0.61.1) -# so this fork stays a clean "upstream + PTY fix". Update via ./sync-upstream.sh. -[dependencies] -aes = "0.9" -async-trait = { version = "0.1.50", optional = true } -aws-lc-rs = { version = "1.16.2", optional = true } -bitflags = "2.0" -block-padding = { version = "0.4" } -byteorder = "1.4" -bytes = "1.7" -cbc = { version = "0.2" } -cipher = "0.5.1" # only pinned due to a cargo-minimal-versions failure in 0.5.0 -ctr = "0.10" -curve25519-dalek = "=5.0.0-pre.6" -crypto-bigint = { version = "0.7.3", features = ["alloc"] } -data-encoding = "2.3" -delegate = "0.13" -digest = "0.11.0-rc.5" -der = "0.8" -des = { version = "0.9", optional = true } -ecdsa = "=0.17.0-rc.18" -ed25519-dalek = { version = "=3.0.0-pre.7", features = ["alloc", "rand_core", "pkcs8"] } -elliptic-curve = { version = "=0.14.0-rc.32", features = ["ecdh"] } -enum_dispatch = "0.3.13" -flate2 = { version = "1.0.15", optional = true } -futures = "0.3" -generic-array = { version = "1.3.3", features = ["compat-0_14"] } -getrandom = { version = "0.2.15", features = ["js"] } -ghash = "0.6.0" # only pinned due to a cargo-minimal-versions failure in 0.6.0-rc.3 -hex-literal = "1" -hkdf = "0.13.0" -hmac = "0.13" -inout = { version = "0.1", features = ["std"] } -keccak = "0.2.0" -log = "0.4.11" -md5 = "0.7" -ml-kem = "0.3" -module-lattice = "0.2" -# num-bigint 0.4.x only supports rand 0.8. The internal-russh-num-bigint fork -# adds rand 0.10 support via the `rand_0_10` feature flag (matches upstream russh). -num-bigint = { package = "internal-russh-num-bigint", version = "=0.5.0", features = ["rand_0_10"] } -num_bigint_0_4 = { package = "num-bigint", version = "0.4.6" } -p256 = { version = "=0.14.0-rc.9", features = ["ecdh"] } -p384 = { version = "=0.14.0-rc.9", features = ["ecdh"] } -p521 = { version = "=0.14.0-rc.9", features = ["ecdh"] } -pbkdf2 = "0.13" -pkcs1 = { version = "=0.8.0-rc.4", optional = true } -pkcs5 = "0.8" -pkcs8 = { version = "0.11", features = ["encryption", "std"] } -polyval = "0.7.1" # only pinned due to a cargo-minimal-versions failure in 0.7.0 -rand_core = { version = "0.10.0" } -rand = { version = "0.10", features = ["thread_rng"] } -ring = { version = "0.17.14", optional = true } -rsa = { version = "=0.10.0-rc.18", optional = true } -salsa20 = "0.11.0" -scrypt = "0.12.0" -sec1 = { version = "0.8", features = ["der"] } -sha1 = { version = "0.11", features = ["oid"] } -sha2 = { version = "0.11", features = ["oid"] } -sha3 = "0.11.0" -signature = "3.0.0-rc.10" -spki = "0.8" -ssh-encoding = { version = "=0.3.0-rc.9", features = ["bytes"] } -subtle = "2.4" -thiserror = "2.0.18" -tokio = { version = "1.52.3", features = ["io-util", "sync", "time", "rt-multi-thread", "net"] } -typenum = "1.17" -universal-hash = "0.6.1" # only pinned due to a cargo-minimal-versions failure in 0.6.0 -yasna = { version = "0.5.0", features = ["bit-vec", "num-bigint"], optional = true } -zeroize = "1.7" - -# Public russh crates (no modifications needed) — pinned to the 0.61.x line. -russh-cryptovec = { version = "0.61.0", features = ["ssh-encoding"] } -russh-util = "0.52.0" - -# ssh-key: upstream russh 0.61.x switched from the internal-russh-forked-ssh-key -# fork back to the real crate at 0.7.0-rc.10 (the fork's changes are upstreamed). -ssh-key = { version = "=0.7.0-rc.10", features = [ - "ed25519", - "p256", - "p384", - "p521", - "encryption", - "ppk", - "sha1", -] } - -[target.'cfg(windows)'.dependencies] -pageant = "0.2" - -[dev-dependencies] -# Dev-deps required by the inline test modules imported from upstream russh -# (src/client/test.rs, src/keys/mod.rs, src/tests.rs). Without these, the fork's -# test target never compiles. -env_logger = "0.11" -tempfile = "3" -# Additive: merges with the main tokio dep to add the `process` and `macros` -# features needed by the spawn-ssh-agent helpers and #[tokio::test]. -tokio = { version = "1.52.3", features = ["process", "macros"] } -# Needed by the PTY-fix regression test (tests/pty_handle_data.rs) to generate -# ephemeral host/client keys; the lib's own `rand` dep isn't visible to test crates. -rand = { version = "0.10", features = ["thread_rng"] } diff --git a/crates/bssh-russh/README.md b/crates/bssh-russh/README.md deleted file mode 100644 index 8b3caaf5..00000000 --- a/crates/bssh-russh/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# bssh-russh - -**Temporary fork of [russh](https://crates.io/crates/russh) (tracking upstream `0.61.1`) with a high-frequency PTY output fix.** - -This crate exists solely to address a specific issue where `Handle::data()` messages from spawned tasks may not be delivered to SSH clients during high-throughput PTY sessions. - -## The Problem - -When implementing SSH servers with interactive PTY support, shell output sent via `Handle::data()` from spawned tasks may not reach the client. The `tokio::select!` in russh's server session loop doesn't always wake up promptly for messages sent through the internal mpsc channel. - -## The Fix - -Added a `try_recv()` batch processing loop before `select!` to drain pending messages, with a limit of 64 messages per batch to maintain input responsiveness (e.g., Ctrl+C). The change lives in `src/server/session.rs` and is re-applied on each sync from `patches/handle-data-fix.patch`. A regression test lives in `tests/pty_handle_data.rs`. - -## Usage - -```toml -[dependencies] -russh = { package = "bssh-russh", version = "0.61.1" } -``` - -## Sync with Upstream - -This fork tracks upstream russh releases. To sync with a new version: - -```bash -cd crates/bssh-russh -./sync-upstream.sh 0.61.1 # specify version -``` - -`sync-upstream.sh` copies upstream `russh/src` verbatim and re-applies every patch under `patches/`. Patches that have already landed upstream are detected (their reverse-apply succeeds) and skipped. As of the `0.61.1` sync, only `handle-data-fix.patch` remains — the previous `channel-write-ordering`, `agent-frame-length-cap`, and `sha1-mac-exclude` cherry-picks are now upstream and were removed. - -## Upstream Status - -- Issue: High-frequency PTY output not delivered when using `Handle::data()` from spawned tasks -- PR: https://github.com/inureyes/russh/tree/fix/handle-data-from-spawned-tasks -- When merged upstream, this fork will be deprecated - -## License - -Apache-2.0 (same as russh) diff --git a/crates/bssh-russh/create-patch.sh b/crates/bssh-russh/create-patch.sh deleted file mode 100755 index 6ed613e8..00000000 --- a/crates/bssh-russh/create-patch.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# create-patch.sh -# Regenerates patches/handle-data-fix.patch by diffing the current vendored -# source against a fresh checkout of upstream russh. -# -# Self-contained: clones upstream into a temp dir (no manually-maintained -# references/ directory needed), so it always diffs against the exact version. -# -# Usage: ./create-patch.sh [version] -# version: optional, e.g. "0.61.1" (default: latest tag) - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -UPSTREAM_URL="https://github.com/warp-tech/russh.git" -TEMP_DIR="/tmp/russh-createpatch-$$" -PATCH_DIR="$SCRIPT_DIR/patches" -PATCH_FILE="$PATCH_DIR/handle-data-fix.patch" - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } - -cleanup() { [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"; } -trap cleanup EXIT - -VERSION="${1:-}" - -log_info "Cloning upstream russh..." -git clone --quiet --depth 100 "$UPSTREAM_URL" "$TEMP_DIR" -cd "$TEMP_DIR" - -if [ -z "$VERSION" ]; then - VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "main") -fi -if [ "$VERSION" != "main" ]; then - git checkout --quiet "v$VERSION" 2>/dev/null || git checkout --quiet "$VERSION" -fi -log_info "Diffing against upstream $VERSION ($(git rev-parse --short HEAD))" - -UPSTREAM_SRC="$TEMP_DIR/russh/src" -CURRENT_SRC="$SCRIPT_DIR/src" -mkdir -p "$PATCH_DIR" - -# The only fork change is the PTY fix in server/session.rs (the batch try_recv -# drain before select!). Emit a -p1 patch with a/ b/ prefixes. -diff -u \ - --label a/src/server/session.rs \ - --label b/src/server/session.rs \ - "$UPSTREAM_SRC/server/session.rs" \ - "$CURRENT_SRC/server/session.rs" \ - > "$PATCH_FILE" || true - -if [ -s "$PATCH_FILE" ]; then - LINES=$(wc -l < "$PATCH_FILE" | tr -d ' ') - log_info "Patch created: $PATCH_FILE ($LINES lines)" - echo "" - echo "Patch summary:" - echo "==============" - grep -E "^@@|^\+\+\+|^---" "$PATCH_FILE" | head -20 -else - log_warn "No differences found - patch file is empty" -fi diff --git a/crates/bssh-russh/patches/handle-data-fix.patch b/crates/bssh-russh/patches/handle-data-fix.patch deleted file mode 100644 index 1f3663a6..00000000 --- a/crates/bssh-russh/patches/handle-data-fix.patch +++ /dev/null @@ -1,148 +0,0 @@ ---- a/src/server/session.rs -+++ b/src/server/session.rs -@@ -6,7 +6,7 @@ - use kex::ServerKex; - use log::debug; - use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; --use tokio::sync::mpsc::{Receiver, Sender, channel}; -+use tokio::sync::mpsc::{Receiver, Sender, channel, error::TryRecvError}; - use tokio::sync::oneshot; - - use super::*; -@@ -511,6 +511,136 @@ - while !self.common.disconnected { - self.common.received_data = false; - let mut sent_keepalive = false; -+ -+ // BSSH FIX: Process pending messages before entering select! -+ // This ensures messages sent via Handle::data() from spawned tasks -+ // are processed even when select! doesn't wake up for them. -+ // Critical for interactive PTY sessions where shell I/O runs in a separate task. -+ // -+ // We limit the number of messages processed per batch to ensure client input -+ // (e.g., Ctrl+C) is handled promptly even during high-throughput output. -+ const MAX_MESSAGES_PER_BATCH: usize = 64; -+ let mut processed_count = 0usize; -+ if !self.kex.active() { -+ loop { -+ if processed_count >= MAX_MESSAGES_PER_BATCH { -+ // Yield to select! to check for client input -+ break; -+ } -+ match self.receiver.try_recv() { -+ Ok(Msg::Channel(id, ChannelMsg::Data { data })) => { -+ self.data(id, data)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::ExtendedData { ext, data })) => { -+ self.extended_data(id, ext, data)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::Eof)) => { -+ self.eof(id)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::Close)) => { -+ self.close(id)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::Success)) => { -+ self.channel_success(id)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::Failure)) => { -+ self.channel_failure(id)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::XonXoff { client_can_do })) => { -+ self.xon_xoff_request(id, client_can_do)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::ExitStatus { exit_status })) => { -+ self.exit_status_request(id, exit_status)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::ExitSignal { signal_name, core_dumped, error_message, lang_tag })) => { -+ self.exit_signal_request(id, signal_name, core_dumped, &error_message, &lang_tag)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Channel(id, ChannelMsg::WindowAdjusted { new_size })) => { -+ debug!("window adjusted to {new_size:?} for channel {id:?}"); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenAgent { channel_ref }) => { -+ let id = self.channel_open_agent()?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenSession { channel_ref }) => { -+ let id = self.channel_open_session()?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenDirectTcpIp { host_to_connect, port_to_connect, originator_address, originator_port, channel_ref }) => { -+ let id = self.channel_open_direct_tcpip(&host_to_connect, port_to_connect, &originator_address, originator_port)?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenDirectStreamLocal { socket_path, channel_ref }) => { -+ let id = self.channel_open_direct_streamlocal(&socket_path)?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenForwardedTcpIp { connected_address, connected_port, originator_address, originator_port, channel_ref }) => { -+ let id = self.channel_open_forwarded_tcpip(&connected_address, connected_port, &originator_address, originator_port)?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenForwardedStreamLocal { server_socket_path, channel_ref }) => { -+ let id = self.channel_open_forwarded_streamlocal(&server_socket_path)?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::ChannelOpenX11 { originator_address, originator_port, channel_ref }) => { -+ let id = self.channel_open_x11(&originator_address, originator_port)?; -+ self.channels.insert(id, channel_ref); -+ processed_count += 1; -+ } -+ Ok(Msg::TcpIpForward { address, port, reply_channel }) => { -+ self.tcpip_forward(&address, port, reply_channel)?; -+ processed_count += 1; -+ } -+ Ok(Msg::CancelTcpIpForward { address, port, reply_channel }) => { -+ self.cancel_tcpip_forward(&address, port, reply_channel)?; -+ processed_count += 1; -+ } -+ Ok(Msg::Disconnect { reason, description, language_tag }) => { -+ self.common.disconnect(reason, &description, &language_tag)?; -+ processed_count += 1; -+ } -+ Ok(_) => { -+ // should be unreachable -+ processed_count += 1; -+ } -+ Err(TryRecvError::Empty) => { -+ // No more pending messages, proceed to select! -+ break; -+ } -+ Err(TryRecvError::Disconnected) => { -+ debug!("receiver disconnected"); -+ break; -+ } -+ } -+ } -+ // Only flush if we actually processed messages -+ if processed_count > 0 { -+ self.flush()?; -+ map_err!( -+ self.common -+ .packet_writer -+ .flush_into(&mut stream_write) -+ .await -+ )?; -+ } -+ } -+ - tokio::select! { - r = &mut reading => { - let (stream_read, mut buffer, mut opening_cipher) = match r { diff --git a/crates/bssh-russh/src/auth.rs b/crates/bssh-russh/src/auth.rs deleted file mode 100644 index c1e3e443..00000000 --- a/crates/bssh-russh/src/auth.rs +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::future::Future; -use std::ops::Deref; -use std::str::FromStr; -use std::sync::Arc; - -use ssh_key::{Certificate, HashAlg, PrivateKey}; -use thiserror::Error; -use tokio::io::{AsyncRead, AsyncWrite}; - -use crate::helpers::NameList; -use crate::keys::PrivateKeyWithHashAlg; -use crate::keys::agent::AgentIdentity; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MethodKind { - None, - Password, - PublicKey, - HostBased, - KeyboardInteractive, -} - -impl From<&MethodKind> for &'static str { - fn from(value: &MethodKind) -> Self { - match value { - MethodKind::None => "none", - MethodKind::Password => "password", - MethodKind::PublicKey => "publickey", - MethodKind::HostBased => "hostbased", - MethodKind::KeyboardInteractive => "keyboard-interactive", - } - } -} - -impl FromStr for MethodKind { - fn from_str(b: &str) -> Result { - match b { - "none" => Ok(MethodKind::None), - "password" => Ok(MethodKind::Password), - "publickey" => Ok(MethodKind::PublicKey), - "hostbased" => Ok(MethodKind::HostBased), - "keyboard-interactive" => Ok(MethodKind::KeyboardInteractive), - _ => Err(()), - } - } - - type Err = (); -} - -impl From<&MethodKind> for String { - fn from(value: &MethodKind) -> Self { - <&str>::from(value).to_string() - } -} - -/// An ordered set of authentication methods. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MethodSet(Vec); - -impl Deref for MethodSet { - type Target = [MethodKind]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From<&[MethodKind]> for MethodSet { - fn from(value: &[MethodKind]) -> Self { - let mut this = Self::empty(); - for method in value { - this.push(*method); - } - this - } -} - -impl From<&MethodSet> for NameList { - fn from(value: &MethodSet) -> Self { - Self(value.iter().map(|x| x.into()).collect()) - } -} - -impl From<&NameList> for MethodSet { - fn from(value: &NameList) -> Self { - Self( - value - .iter() - .filter_map(|x| MethodKind::from_str(x).ok()) - .collect(), - ) - } -} - -impl MethodSet { - pub fn empty() -> Self { - Self(Vec::new()) - } - - pub fn all() -> Self { - Self(vec![ - MethodKind::None, - MethodKind::Password, - MethodKind::PublicKey, - MethodKind::HostBased, - MethodKind::KeyboardInteractive, - ]) - } - - pub fn remove(&mut self, method: MethodKind) { - self.0.retain(|x| *x != method); - } - - /// Push a method to the end of the list. - /// If the method is already in the list, it is moved to the end. - pub fn push(&mut self, method: MethodKind) { - self.remove(method); - self.0.push(method); - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AuthResult { - Success, - Failure { - /// The server suggests to proceed with these auth methods - remaining_methods: MethodSet, - /// The server says that though auth method has been accepted, - /// further authentication is required - partial_success: bool, - }, -} - -impl AuthResult { - pub fn success(&self) -> bool { - matches!(self, AuthResult::Success) - } -} - -#[cfg_attr(feature = "async-trait", async_trait::async_trait)] -pub trait Signer: Sized { - type Error: From; - - fn auth_sign( - &mut self, - key: &AgentIdentity, - hash_alg: Option, - to_sign: Vec, - ) -> impl Future, Self::Error>> + Send; -} - -#[derive(Debug, Error)] -pub enum AgentAuthError { - #[error(transparent)] - Send(#[from] crate::SendError), - #[error(transparent)] - Key(#[from] crate::keys::Error), -} - -#[cfg_attr(feature = "async-trait", async_trait::async_trait)] -impl Signer - for crate::keys::agent::client::AgentClient -{ - type Error = AgentAuthError; - - #[allow(clippy::manual_async_fn)] - fn auth_sign( - &mut self, - key: &AgentIdentity, - hash_alg: Option, - to_sign: Vec, - ) -> impl Future, Self::Error>> { - async move { - self.sign_request(key, hash_alg, to_sign) - .await - .map_err(Into::into) - } - } -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum Method { - None, - Password { - password: String, - }, - PublicKey { - key: PrivateKeyWithHashAlg, - }, - OpenSshCertificate { - key: Arc, - cert: Certificate, - }, - FuturePublicKey { - key: ssh_key::PublicKey, - hash_alg: Option, - }, - /// Certificate-based authentication using an external signer (e.g., SSH agent). - /// The certificate is sent to the server, but signing is delegated to the signer. - FutureCertificate { - cert: Certificate, - hash_alg: Option, - }, - KeyboardInteractive { - submethods: String, - }, - // Hostbased, -} - -#[doc(hidden)] -#[derive(Debug)] -pub struct AuthRequest { - initial_methods: MethodSet, - pub methods: MethodSet, - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - pub partial_success: bool, - pub current: Option, - pub(crate) principal: Option, - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - pub rejection_count: usize, -} - -#[doc(hidden)] -#[derive(Debug)] -pub(crate) struct AuthPrincipal { - user: String, - service: String, -} - -#[doc(hidden)] -#[derive(Debug)] -pub enum CurrentRequest { - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - PublicKey { - #[allow(dead_code)] - key: Vec, - #[allow(dead_code)] - algo: Vec, - sent_pk_ok: bool, - }, - KeyboardInteractive { - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - submethods: String, - }, -} - -impl AuthRequest { - pub(crate) fn server(methods: MethodSet) -> Self { - Self { - initial_methods: methods.clone(), - methods, - partial_success: false, - current: None, - principal: None, - rejection_count: 0, - } - } - - pub(crate) fn new(method: &Method) -> Self { - match method { - Method::KeyboardInteractive { submethods } => Self { - initial_methods: MethodSet::all(), - methods: MethodSet::all(), - partial_success: false, - current: Some(CurrentRequest::KeyboardInteractive { - submethods: submethods.to_string(), - }), - principal: None, - rejection_count: 0, - }, - _ => Self { - initial_methods: MethodSet::all(), - methods: MethodSet::all(), - partial_success: false, - current: None, - principal: None, - rejection_count: 0, - }, - } - } - - pub(crate) fn bind_or_reset_principal(&mut self, user: &str, service: &str) -> bool { - match &self.principal { - Some(bound) if bound.user == user && bound.service == service => false, - _ => { - self.principal = Some(AuthPrincipal { - user: user.to_owned(), - service: service.to_owned(), - }); - self.methods = self.initial_methods.clone(); - self.partial_success = false; - self.current = None; - true - } - } - } -} diff --git a/crates/bssh-russh/src/cert.rs b/crates/bssh-russh/src/cert.rs deleted file mode 100644 index 2a101049..00000000 --- a/crates/bssh-russh/src/cert.rs +++ /dev/null @@ -1,46 +0,0 @@ -use ssh_key::{Certificate, HashAlg, PublicKey}; -#[cfg(not(target_arch = "wasm32"))] -use { - crate::helpers::AlgorithmExt, ssh_encoding::Decode, ssh_key::Algorithm, - ssh_key::public::KeyData, -}; - -use crate::keys::key::PrivateKeyWithHashAlg; - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub(crate) enum PublicKeyOrCertificate { - PublicKey { - key: PublicKey, - hash_alg: Option, - }, - Certificate(Certificate), -} - -impl From<&PrivateKeyWithHashAlg> for PublicKeyOrCertificate { - fn from(key: &PrivateKeyWithHashAlg) -> Self { - PublicKeyOrCertificate::PublicKey { - key: key.public_key().clone(), - hash_alg: key.hash_alg(), - } - } -} - -impl PublicKeyOrCertificate { - #[cfg(not(target_arch = "wasm32"))] - pub fn decode(pubkey_algo: &str, buf: &[u8]) -> Result { - let mut reader = buf; - match Algorithm::new_certificate_ext(pubkey_algo) { - Ok(Algorithm::Other(_)) | Err(ssh_key::Error::Encoding(_)) => { - // Did not match a known cert algorithm - Ok(PublicKeyOrCertificate::PublicKey { - key: KeyData::decode(&mut reader)?.into(), - hash_alg: Algorithm::new(pubkey_algo)?.hash_alg(), - }) - } - _ => Ok(PublicKeyOrCertificate::Certificate(Certificate::decode( - &mut reader, - )?)), - } - } -} diff --git a/crates/bssh-russh/src/channels/channel_ref.rs b/crates/bssh-russh/src/channels/channel_ref.rs deleted file mode 100644 index d7f937cd..00000000 --- a/crates/bssh-russh/src/channels/channel_ref.rs +++ /dev/null @@ -1,33 +0,0 @@ -use tokio::sync::mpsc::Sender; - -use super::WindowSizeRef; -use crate::ChannelMsg; - -/// A handle to the [`super::Channel`]'s to be able to transmit messages -/// to it and update it's `window_size`. -#[derive(Debug)] -pub struct ChannelRef { - pub(super) sender: Sender, - pub(super) window_size: WindowSizeRef, -} - -impl ChannelRef { - pub fn new(sender: Sender) -> Self { - Self { - sender, - window_size: WindowSizeRef::new(0), - } - } - - pub(crate) fn window_size(&self) -> &WindowSizeRef { - &self.window_size - } -} - -impl std::ops::Deref for ChannelRef { - type Target = Sender; - - fn deref(&self) -> &Self::Target { - &self.sender - } -} diff --git a/crates/bssh-russh/src/channels/channel_stream.rs b/crates/bssh-russh/src/channels/channel_stream.rs deleted file mode 100644 index 9e8d14be..00000000 --- a/crates/bssh-russh/src/channels/channel_stream.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use tokio::io::{AsyncRead, AsyncWrite}; - -use super::io::{ChannelCloseOnDrop, ChannelRx, ChannelTx}; -use super::{ChannelId, ChannelMsg}; - -/// AsyncRead/AsyncWrite wrapper for SSH Channels -pub struct ChannelStream -where - S: From<(ChannelId, ChannelMsg)> + Send + 'static, -{ - tx: ChannelTx, - rx: ChannelRx>, -} - -impl ChannelStream -where - S: From<(ChannelId, ChannelMsg)> + Send, -{ - pub(super) fn new(tx: ChannelTx, rx: ChannelRx>) -> Self { - Self { tx, rx } - } -} - -impl AsyncRead for ChannelStream -where - S: From<(ChannelId, ChannelMsg)> + Send, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.rx).poll_read(cx, buf) - } -} - -impl AsyncWrite for ChannelStream -where - S: From<(ChannelId, ChannelMsg)> + 'static + Send + Sync, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.tx).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.tx).poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(&mut self.tx).poll_shutdown(cx) - } -} diff --git a/crates/bssh-russh/src/channels/io/mod.rs b/crates/bssh-russh/src/channels/io/mod.rs deleted file mode 100644 index 95aeab50..00000000 --- a/crates/bssh-russh/src/channels/io/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -mod rx; -use std::borrow::{Borrow, BorrowMut}; - -pub use rx::ChannelRx; - -mod tx; -pub use tx::ChannelTx; - -use crate::{Channel, ChannelId, ChannelMsg, ChannelReadHalf}; - -#[derive(Debug)] -pub struct ChannelCloseOnDrop + Send + 'static>(pub Channel); - -impl + Send + 'static> Borrow - for ChannelCloseOnDrop -{ - fn borrow(&self) -> &ChannelReadHalf { - &self.0.read_half - } -} - -impl + Send + 'static> BorrowMut - for ChannelCloseOnDrop -{ - fn borrow_mut(&mut self) -> &mut ChannelReadHalf { - &mut self.0.read_half - } -} - -impl + Send + 'static> Drop for ChannelCloseOnDrop { - fn drop(&mut self) { - let id = self.0.write_half.id; - let sender = self.0.write_half.sender.clone(); - - // Best effort: async drop where possible - #[cfg(not(target_arch = "wasm32"))] - tokio::spawn(async move { - let _ = sender.send((id, ChannelMsg::Close).into()).await; - }); - - #[cfg(target_arch = "wasm32")] - let _ = sender.try_send((id, ChannelMsg::Close).into()); - } -} diff --git a/crates/bssh-russh/src/channels/io/rx.rs b/crates/bssh-russh/src/channels/io/rx.rs deleted file mode 100644 index 57080db5..00000000 --- a/crates/bssh-russh/src/channels/io/rx.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::borrow::BorrowMut; -use std::io; -use std::pin::Pin; -use std::task::{ready, Context, Poll}; - -use tokio::io::AsyncRead; - -use super::{ChannelMsg, ChannelReadHalf}; - -#[derive(Debug)] -pub struct ChannelRx { - channel: R, - buffer: Option<(ChannelMsg, usize)>, - - ext: Option, -} - -impl ChannelRx { - pub fn new(channel: R, ext: Option) -> Self { - Self { - channel, - buffer: None, - ext, - } - } -} - -impl AsyncRead for ChannelRx -where - R: BorrowMut + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - let (msg, mut idx) = match self.buffer.take() { - Some(msg) => msg, - None => match ready!(self.channel.borrow_mut().receiver.poll_recv(cx)) { - Some(msg) => (msg, 0), - None => return Poll::Ready(Ok(())), - }, - }; - - match (&msg, self.ext) { - (ChannelMsg::Data { data }, None) => { - let readable = buf.remaining().min(data.len() - idx); - - // Clamped to maximum `buf.remaining()` and `data.len() - idx` with `.min` - #[allow(clippy::indexing_slicing)] - buf.put_slice(&data[idx..idx + readable]); - idx += readable; - - if idx != data.len() { - self.buffer = Some((msg, idx)); - } - - Poll::Ready(Ok(())) - } - (ChannelMsg::ExtendedData { data, ext }, Some(target)) if *ext == target => { - let readable = buf.remaining().min(data.len() - idx); - - // Clamped to maximum `buf.remaining()` and `data.len() - idx` with `.min` - #[allow(clippy::indexing_slicing)] - buf.put_slice(&data[idx..idx + readable]); - idx += readable; - - if idx != data.len() { - self.buffer = Some((msg, idx)); - } - - Poll::Ready(Ok(())) - } - (ChannelMsg::Eof, _) => { - self.channel.borrow_mut().receiver.close(); - - Poll::Ready(Ok(())) - } - _ => { - cx.waker().wake_by_ref(); - Poll::Pending - } - } - } -} diff --git a/crates/bssh-russh/src/channels/io/tx.rs b/crates/bssh-russh/src/channels/io/tx.rs deleted file mode 100644 index 7b8a56fc..00000000 --- a/crates/bssh-russh/src/channels/io/tx.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::convert::TryFrom; -use std::future::Future; -use std::io; -use std::num::NonZeroUsize; -use std::ops::DerefMut; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{ready, Context, Poll}; - -use futures::FutureExt; -use tokio::io::AsyncWrite; -use tokio::sync::mpsc::error::SendError; -use tokio::sync::mpsc::{self, OwnedPermit}; -use tokio::sync::{Mutex, Notify, OwnedMutexGuard}; - -use bytes::Bytes; - -use super::ChannelMsg; -use crate::ChannelId; - -type BoxedThreadsafeFuture = Pin>>; -type OwnedPermitFuture = - BoxedThreadsafeFuture, ChannelMsg, usize), SendError<()>>>; - -struct WatchNotification(Pin>>); - -/// A single future that becomes ready once the window size -/// changes to a positive value -impl WatchNotification { - fn new(n: Arc) -> Self { - Self(Box::pin(async move { n.notified().await })) - } -} - -impl Future for WatchNotification { - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let inner = self.deref_mut().0.as_mut(); - ready!(inner.poll(cx)); - Poll::Ready(()) - } -} - -pub struct ChannelTx { - sender: mpsc::Sender, - send_fut: Option>, - id: ChannelId, - window_size_fut: Option>>, - window_size: Arc>, - notify: Arc, - window_size_notication: WatchNotification, - max_packet_size: u32, - ext: Option, -} - -impl ChannelTx -where - S: From<(ChannelId, ChannelMsg)> + 'static + Send, -{ - pub fn new( - sender: mpsc::Sender, - id: ChannelId, - window_size: Arc>, - window_size_notification: Arc, - max_packet_size: u32, - ext: Option, - ) -> Self { - Self { - sender, - send_fut: None, - id, - notify: Arc::clone(&window_size_notification), - window_size_notication: WatchNotification::new(window_size_notification), - window_size, - window_size_fut: None, - max_packet_size, - ext, - } - } - - fn poll_writable(&mut self, cx: &mut Context<'_>, buf_len: usize) -> Poll { - let window_size = self.window_size.clone(); - let window_size_fut = self - .window_size_fut - .get_or_insert_with(|| Box::pin(window_size.lock_owned())); - let mut window_size = ready!(window_size_fut.poll_unpin(cx)); - self.window_size_fut.take(); - - let writable = (self.max_packet_size).min(*window_size).min(buf_len as u32) as usize; - - match NonZeroUsize::try_from(writable) { - Ok(w) => { - *window_size -= writable as u32; - if *window_size > 0 { - self.notify.notify_one(); - } - Poll::Ready(w) - } - Err(_) => { - drop(window_size); - ready!(self.window_size_notication.poll_unpin(cx)); - self.window_size_notication = WatchNotification::new(Arc::clone(&self.notify)); - cx.waker().wake_by_ref(); - Poll::Pending - } - } - } - - fn poll_mk_msg( - &mut self, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll<(ChannelMsg, NonZeroUsize)> { - let writable = ready!(self.poll_writable(cx, buf.len())); - - #[allow(clippy::indexing_slicing)] // Clamped to maximum `buf.len()` with `.poll_writable` - let data = Bytes::copy_from_slice(&buf[..writable.into()]); - - let msg = match self.ext { - None => ChannelMsg::Data { data }, - Some(ext) => ChannelMsg::ExtendedData { data, ext }, - }; - - Poll::Ready((msg, writable)) - } - - fn activate(&mut self, msg: ChannelMsg, writable: usize) -> &mut OwnedPermitFuture { - use futures::TryFutureExt; - self.send_fut.insert(Box::pin( - self.sender - .clone() - .reserve_owned() - .map_ok(move |p| (p, msg, writable)), - )) - } - - fn handle_write_result( - &mut self, - r: Result<(OwnedPermit, ChannelMsg, usize), SendError<()>>, - ) -> Result { - self.send_fut = None; - match r { - Ok((permit, msg, writable)) => { - permit.send((self.id, msg).into()); - Ok(writable) - } - Err(SendError(())) => Err(io::Error::new(io::ErrorKind::BrokenPipe, "channel closed")), - } - } -} - -impl AsyncWrite for ChannelTx -where - S: From<(ChannelId, ChannelMsg)> + 'static + Send, -{ - #[allow(clippy::too_many_lines)] - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - if buf.is_empty() { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::WriteZero, - "cannot send empty buffer", - ))); - } - let send_fut = if let Some(x) = self.send_fut.as_mut() { - x - } else { - let (msg, writable) = ready!(self.poll_mk_msg(cx, buf)); - self.activate(msg, writable.into()) - }; - let r = ready!(send_fut.as_mut().poll_unpin(cx)); - Poll::Ready(self.handle_write_result(r)) - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - let send_fut = if let Some(x) = self.send_fut.as_mut() { - x - } else { - self.activate(ChannelMsg::Eof, 0) - }; - let r = ready!(send_fut.as_mut().poll_unpin(cx)).map(|(p, _, _)| (p, ChannelMsg::Eof, 0)); - Poll::Ready(self.handle_write_result(r).map(drop)) - } -} - -impl Drop for ChannelTx { - fn drop(&mut self) { - // Allow other writers to make progress - self.notify.notify_one(); - } -} diff --git a/crates/bssh-russh/src/channels/mod.rs b/crates/bssh-russh/src/channels/mod.rs deleted file mode 100644 index 2573e238..00000000 --- a/crates/bssh-russh/src/channels/mod.rs +++ /dev/null @@ -1,843 +0,0 @@ -use std::sync::Arc; - -use bytes::Bytes; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::sync::mpsc::{Receiver, Sender}; -use tokio::sync::{Mutex, Notify}; - -use crate::{ChannelId, ChannelOpenFailure, Error, Pty, Sig}; - -pub mod io; - -mod channel_ref; -pub use channel_ref::ChannelRef; - -mod channel_stream; -pub use channel_stream::ChannelStream; - -#[derive(Debug)] -#[non_exhaustive] -/// Possible messages that [Channel::wait] can receive. -pub enum ChannelMsg { - Open { - id: ChannelId, - max_packet_size: u32, - window_size: u32, - }, - Data { - data: Bytes, - }, - ExtendedData { - data: Bytes, - ext: u32, - }, - Eof, - Close, - /// (client only) - RequestPty { - want_reply: bool, - term: String, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - terminal_modes: Vec<(Pty, u32)>, - }, - /// (client only) - RequestShell { - want_reply: bool, - }, - /// (client only) - Exec { - want_reply: bool, - command: Vec, - }, - /// (client only) - Signal { - signal: Sig, - }, - /// (client only) - RequestSubsystem { - want_reply: bool, - name: String, - }, - /// (client only) - RequestX11 { - want_reply: bool, - single_connection: bool, - x11_authentication_protocol: String, - x11_authentication_cookie: String, - x11_screen_number: u32, - }, - /// (client only) - SetEnv { - want_reply: bool, - variable_name: String, - variable_value: String, - }, - /// (client only) - WindowChange { - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - }, - /// (client only) - AgentForward { - want_reply: bool, - }, - - /// (server only) - XonXoff { - client_can_do: bool, - }, - /// (server only) - ExitStatus { - exit_status: u32, - }, - /// (server only) - ExitSignal { - signal_name: Sig, - core_dumped: bool, - error_message: String, - lang_tag: String, - }, - /// (server only) - WindowAdjusted { - new_size: u32, - }, - /// (server only) - Success, - /// (server only) - Failure, - OpenFailure(ChannelOpenFailure), -} - -#[derive(Clone, Debug)] -pub(crate) struct WindowSizeRef { - value: Arc>, - notifier: Arc, -} - -impl WindowSizeRef { - pub(crate) fn new(initial: u32) -> Self { - let notifier = Arc::new(Notify::new()); - Self { - value: Arc::new(Mutex::new(initial)), - notifier, - } - } - - pub(crate) async fn update(&self, value: u32) { - *self.value.lock().await = value; - self.notifier.notify_one(); - } - - pub(crate) fn subscribe(&self) -> Arc { - Arc::clone(&self.notifier) - } -} - -/// A handle to the reading part of a session channel. -/// -/// Allows you to read from a channel without borrowing the session -pub struct ChannelReadHalf { - pub(crate) receiver: Receiver, -} - -impl std::fmt::Debug for ChannelReadHalf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChannelReadHalf").finish() - } -} - -impl ChannelReadHalf { - /// Awaits an incoming [`ChannelMsg`], this method returns [`None`] if the channel has been closed. - pub async fn wait(&mut self) -> Option { - self.receiver.recv().await - } - - /// Make a reader for the [`Channel`] to receive [`ChannelMsg::Data`] - /// through the `AsyncRead` trait. - pub fn make_reader(&mut self) -> impl AsyncRead + '_ { - self.make_reader_ext(None) - } - - /// Make a reader for the [`Channel`] to receive [`ChannelMsg::Data`] or [`ChannelMsg::ExtendedData`] - /// depending on the `ext` parameter, through the `AsyncRead` trait. - pub fn make_reader_ext(&mut self, ext: Option) -> impl AsyncRead + '_ { - io::ChannelRx::new(self, ext) - } -} - -/// A handle to the writing part of a session channel. -/// -/// Allows you to write to a channel without borrowing the session -pub struct ChannelWriteHalf> { - pub(crate) id: ChannelId, - pub(crate) sender: Sender, - pub(crate) max_packet_size: u32, - pub(crate) window_size: WindowSizeRef, -} - -impl> std::fmt::Debug for ChannelWriteHalf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChannelWriteHalf") - .field("id", &self.id) - .finish() - } -} - -impl + Send + Sync + 'static> ChannelWriteHalf { - /// Returns the min between the maximum packet size and the - /// remaining window size in the channel. - pub async fn writable_packet_size(&self) -> usize { - self.max_packet_size - .min(*self.window_size.value.lock().await) as usize - } - - pub fn id(&self) -> ChannelId { - self.id - } - - /// Request a pseudo-terminal with the given characteristics. - #[allow(clippy::too_many_arguments)] // length checked - pub async fn request_pty( - &self, - want_reply: bool, - term: &str, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - terminal_modes: &[(Pty, u32)], - ) -> Result<(), Error> { - self.send_msg(ChannelMsg::RequestPty { - want_reply, - term: term.to_string(), - col_width, - row_height, - pix_width, - pix_height, - terminal_modes: terminal_modes.to_vec(), - }) - .await - } - - /// Request a remote shell. - pub async fn request_shell(&self, want_reply: bool) -> Result<(), Error> { - self.send_msg(ChannelMsg::RequestShell { want_reply }).await - } - - /// Execute a remote program (will be passed to a shell). This can - /// be used to implement scp (by calling a remote scp and - /// tunneling to its standard input). - pub async fn exec>>(&self, want_reply: bool, command: A) -> Result<(), Error> { - self.send_msg(ChannelMsg::Exec { - want_reply, - command: command.into(), - }) - .await - } - - /// Signal a remote process. - pub async fn signal(&self, signal: Sig) -> Result<(), Error> { - self.send_msg(ChannelMsg::Signal { signal }).await - } - - /// Request the start of a subsystem with the given name. - pub async fn request_subsystem>( - &self, - want_reply: bool, - name: A, - ) -> Result<(), Error> { - self.send_msg(ChannelMsg::RequestSubsystem { - want_reply, - name: name.into(), - }) - .await - } - - /// Request X11 forwarding through an already opened X11 - /// channel. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.3.1) - /// for security issues related to cookies. - pub async fn request_x11, B: Into>( - &self, - want_reply: bool, - single_connection: bool, - x11_authentication_protocol: A, - x11_authentication_cookie: B, - x11_screen_number: u32, - ) -> Result<(), Error> { - self.send_msg(ChannelMsg::RequestX11 { - want_reply, - single_connection, - x11_authentication_protocol: x11_authentication_protocol.into(), - x11_authentication_cookie: x11_authentication_cookie.into(), - x11_screen_number, - }) - .await - } - - /// Set a remote environment variable. - pub async fn set_env, B: Into>( - &self, - want_reply: bool, - variable_name: A, - variable_value: B, - ) -> Result<(), Error> { - self.send_msg(ChannelMsg::SetEnv { - want_reply, - variable_name: variable_name.into(), - variable_value: variable_value.into(), - }) - .await - } - - /// Inform the server that our window size has changed. - pub async fn window_change( - &self, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - ) -> Result<(), Error> { - self.send_msg(ChannelMsg::WindowChange { - col_width, - row_height, - pix_width, - pix_height, - }) - .await - } - - /// Inform the server that we will accept agent forwarding channels - pub async fn agent_forward(&self, want_reply: bool) -> Result<(), Error> { - self.send_msg(ChannelMsg::AgentForward { want_reply }).await - } - - /// Send data to a channel. - pub async fn data(&self, data: R) -> Result<(), Error> { - self.send_data(None, data).await - } - - /// Send owned bytes to a channel without copying them into the `AsyncWrite` path. - pub async fn data_bytes(&self, data: impl Into) -> Result<(), Error> { - self.send_bytes(None, data.into()).await - } - - /// Send data to a channel. The number of bytes added to the - /// "sending pipeline" (to be processed by the event loop) is - /// returned. - pub async fn extended_data( - &self, - ext: u32, - data: R, - ) -> Result<(), Error> { - self.send_data(Some(ext), data).await - } - - /// Send owned extended data to a channel without copying it into the `AsyncWrite` path. - pub async fn extended_data_bytes( - &self, - ext: u32, - data: impl Into, - ) -> Result<(), Error> { - self.send_bytes(Some(ext), data.into()).await - } - - async fn send_data( - &self, - ext: Option, - mut data: R, - ) -> Result<(), Error> { - let mut tx = self.make_writer_ext(ext); - - tokio::io::copy(&mut data, &mut tx).await?; - - Ok(()) - } - - async fn reserve_writable_chunk(&self, remaining: usize) -> Result { - if self.max_packet_size == 0 { - return Err(Error::Inconsistent); - } - loop { - let mut window_size = self.window_size.value.lock().await; - let writable = (self.max_packet_size as usize) - .min(*window_size as usize) - .min(remaining); - if writable > 0 { - *window_size -= writable as u32; - if *window_size > 0 { - self.window_size.notifier.notify_one(); - } - return Ok(writable); - } - let notified = self.window_size.notifier.notified(); - drop(window_size); - notified.await; - } - } - - async fn send_bytes(&self, ext: Option, data: Bytes) -> Result<(), Error> { - if data.is_empty() { - return Ok(()); - } - - let mut offset = 0; - while offset < data.len() { - let writable = self.reserve_writable_chunk(data.len() - offset).await?; - let end = offset + writable; - let chunk = data.slice(offset..end); - let msg = match ext { - None => ChannelMsg::Data { data: chunk }, - Some(ext) => ChannelMsg::ExtendedData { data: chunk, ext }, - }; - self.send_msg(msg).await?; - offset = end; - } - - Ok(()) - } - - pub async fn eof(&self) -> Result<(), Error> { - self.send_msg(ChannelMsg::Eof).await - } - - pub async fn exit_status(&self, exit_status: u32) -> Result<(), Error> { - self.send_msg(ChannelMsg::ExitStatus { exit_status }).await - } - - /// Request that the channel be closed. - pub async fn close(&self) -> Result<(), Error> { - self.send_msg(ChannelMsg::Close).await - } - - async fn send_msg(&self, msg: ChannelMsg) -> Result<(), Error> { - self.sender - .send((self.id, msg).into()) - .await - .map_err(|_| Error::SendError) - } - - /// Make a writer for the [`Channel`] to send [`ChannelMsg::Data`] - /// through the `AsyncWrite` trait. - pub fn make_writer(&self) -> impl AsyncWrite + 'static { - self.make_writer_ext(None) - } - - /// Make a writer for the [`Channel`] to send [`ChannelMsg::Data`] or [`ChannelMsg::ExtendedData`] - /// depending on the `ext` parameter, through the `AsyncWrite` trait. - pub fn make_writer_ext(&self, ext: Option) -> impl AsyncWrite + 'static { - io::ChannelTx::new( - self.sender.clone(), - self.id, - self.window_size.value.clone(), - self.window_size.subscribe(), - self.max_packet_size, - ext, - ) - } -} - -/// A handle to a session channel. -/// -/// Allows you to read and write from a channel without borrowing the session -pub struct Channel> { - pub(crate) read_half: ChannelReadHalf, - pub(crate) write_half: ChannelWriteHalf, -} - -impl> std::fmt::Debug for Channel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Channel") - .field("id", &self.write_half.id) - .finish() - } -} - -impl + Send + Sync + 'static> Channel { - pub(crate) fn new( - id: ChannelId, - sender: Sender, - max_packet_size: u32, - window_size: u32, - channel_buffer_size: usize, - ) -> (Self, ChannelRef) { - let (tx, rx) = tokio::sync::mpsc::channel(channel_buffer_size); - let window_size = WindowSizeRef::new(window_size); - let read_half = ChannelReadHalf { receiver: rx }; - let write_half = ChannelWriteHalf { - id, - sender, - max_packet_size, - window_size: window_size.clone(), - }; - - ( - Self { - write_half, - read_half, - }, - ChannelRef { - sender: tx, - window_size, - }, - ) - } - - /// Returns the min between the maximum packet size and the - /// remaining window size in the channel. - pub async fn writable_packet_size(&self) -> usize { - self.write_half.writable_packet_size().await - } - - pub fn id(&self) -> ChannelId { - self.write_half.id() - } - - /// Split this [`Channel`] into a [`ChannelReadHalf`] and a [`ChannelWriteHalf`], which can be - /// used to read and write concurrently. - pub fn split(self) -> (ChannelReadHalf, ChannelWriteHalf) { - (self.read_half, self.write_half) - } - - /// Request a pseudo-terminal with the given characteristics. - #[allow(clippy::too_many_arguments)] // length checked - pub async fn request_pty( - &self, - want_reply: bool, - term: &str, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - terminal_modes: &[(Pty, u32)], - ) -> Result<(), Error> { - self.write_half - .request_pty( - want_reply, - term, - col_width, - row_height, - pix_width, - pix_height, - terminal_modes, - ) - .await - } - - /// Request a remote shell. - pub async fn request_shell(&self, want_reply: bool) -> Result<(), Error> { - self.write_half.request_shell(want_reply).await - } - - /// Execute a remote program (will be passed to a shell). This can - /// be used to implement scp (by calling a remote scp and - /// tunneling to its standard input). - pub async fn exec>>(&self, want_reply: bool, command: A) -> Result<(), Error> { - self.write_half.exec(want_reply, command).await - } - - /// Signal a remote process. - pub async fn signal(&self, signal: Sig) -> Result<(), Error> { - self.write_half.signal(signal).await - } - - /// Request the start of a subsystem with the given name. - pub async fn request_subsystem>( - &self, - want_reply: bool, - name: A, - ) -> Result<(), Error> { - self.write_half.request_subsystem(want_reply, name).await - } - - /// Request X11 forwarding through an already opened X11 - /// channel. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.3.1) - /// for security issues related to cookies. - pub async fn request_x11, B: Into>( - &self, - want_reply: bool, - single_connection: bool, - x11_authentication_protocol: A, - x11_authentication_cookie: B, - x11_screen_number: u32, - ) -> Result<(), Error> { - self.write_half - .request_x11( - want_reply, - single_connection, - x11_authentication_protocol, - x11_authentication_cookie, - x11_screen_number, - ) - .await - } - - /// Set a remote environment variable. - pub async fn set_env, B: Into>( - &self, - want_reply: bool, - variable_name: A, - variable_value: B, - ) -> Result<(), Error> { - self.write_half - .set_env(want_reply, variable_name, variable_value) - .await - } - - /// Inform the server that our window size has changed. - pub async fn window_change( - &self, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - ) -> Result<(), Error> { - self.write_half - .window_change(col_width, row_height, pix_width, pix_height) - .await - } - - /// Inform the server that we will accept agent forwarding channels - pub async fn agent_forward(&self, want_reply: bool) -> Result<(), Error> { - self.write_half.agent_forward(want_reply).await - } - - /// Send data to a channel. - pub async fn data(&self, data: R) -> Result<(), Error> { - self.write_half.data(data).await - } - - /// Send owned bytes to a channel without copying them into the `AsyncWrite` path. - pub async fn data_bytes(&self, data: impl Into) -> Result<(), Error> { - self.write_half.data_bytes(data).await - } - - /// Send data to a channel. The number of bytes added to the - /// "sending pipeline" (to be processed by the event loop) is - /// returned. - pub async fn extended_data( - &self, - ext: u32, - data: R, - ) -> Result<(), Error> { - self.write_half.extended_data(ext, data).await - } - - /// Send owned extended data to a channel without copying it into the `AsyncWrite` path. - pub async fn extended_data_bytes( - &self, - ext: u32, - data: impl Into, - ) -> Result<(), Error> { - self.write_half.extended_data_bytes(ext, data).await - } - - pub async fn eof(&self) -> Result<(), Error> { - self.write_half.eof().await - } - - pub async fn exit_status(&self, exit_status: u32) -> Result<(), Error> { - self.write_half.exit_status(exit_status).await - } - - /// Request that the channel be closed. - pub async fn close(&self) -> Result<(), Error> { - self.write_half.close().await - } - - /// Awaits an incoming [`ChannelMsg`], this method returns [`None`] if the channel has been closed. - pub async fn wait(&mut self) -> Option { - self.read_half.wait().await - } - - /// Consume the [`Channel`] to produce a bidirectionnal stream, - /// sending and receiving [`ChannelMsg::Data`] as `AsyncRead` + `AsyncWrite`. - pub fn into_stream(self) -> ChannelStream { - ChannelStream::new( - io::ChannelTx::new( - self.write_half.sender.clone(), - self.write_half.id, - self.write_half.window_size.value.clone(), - self.write_half.window_size.subscribe(), - self.write_half.max_packet_size, - None, - ), - io::ChannelRx::new(io::ChannelCloseOnDrop(self), None), - ) - } - - /// Make a reader for the [`Channel`] to receive [`ChannelMsg::Data`] - /// through the `AsyncRead` trait. - pub fn make_reader(&mut self) -> impl AsyncRead + '_ { - self.read_half.make_reader() - } - - /// Make a reader for the [`Channel`] to receive [`ChannelMsg::Data`] or [`ChannelMsg::ExtendedData`] - /// depending on the `ext` parameter, through the `AsyncRead` trait. - pub fn make_reader_ext(&mut self, ext: Option) -> impl AsyncRead + '_ { - self.read_half.make_reader_ext(ext) - } - - /// Make a writer for the [`Channel`] to send [`ChannelMsg::Data`] - /// through the `AsyncWrite` trait. - pub fn make_writer(&self) -> impl AsyncWrite + 'static { - self.write_half.make_writer() - } - - /// Make a writer for the [`Channel`] to send [`ChannelMsg::Data`] or [`ChannelMsg::ExtendedData`] - /// depending on the `ext` parameter, through the `AsyncWrite` trait. - pub fn make_writer_ext(&self, ext: Option) -> impl AsyncWrite + 'static { - self.write_half.make_writer_ext(ext) - } -} - -#[cfg(test)] -mod tests { - use tokio::sync::mpsc; - - use super::*; - - fn test_write_half( - window_size: WindowSizeRef, - max_packet_size: u32, - ) -> ( - ChannelWriteHalf<(ChannelId, ChannelMsg)>, - mpsc::Receiver<(ChannelId, ChannelMsg)>, - ) { - let (sender, receiver) = mpsc::channel(8); - ( - ChannelWriteHalf { - id: ChannelId(7), - sender, - max_packet_size, - window_size, - }, - receiver, - ) - } - - #[tokio::test] - async fn data_bytes_sends_one_owned_message_when_window_permits() { - let payload = Bytes::from_static(b"owned data"); - let (write_half, mut receiver) = test_write_half(WindowSizeRef::new(1024), 1024); - - write_half.data_bytes(payload.clone()).await.unwrap(); - - match receiver.recv().await.unwrap() { - (ChannelId(7), ChannelMsg::Data { data }) => { - assert_eq!(data, payload); - assert_eq!(data.as_ptr(), payload.as_ptr()); - } - msg => panic!("unexpected message: {msg:?}"), - } - } - - #[tokio::test] - async fn data_bytes_splits_by_max_packet_size_without_copying() { - let payload = Bytes::from_static(b"abcdefghij"); - let (write_half, mut receiver) = test_write_half(WindowSizeRef::new(1024), 4); - - write_half.data_bytes(payload.clone()).await.unwrap(); - - for (range, expected) in [ - (0..4, &b"abcd"[..]), - (4..8, &b"efgh"[..]), - (8..10, &b"ij"[..]), - ] { - match receiver.recv().await.unwrap() { - (ChannelId(7), ChannelMsg::Data { data }) => { - assert_eq!(data.as_ref(), expected); - assert_eq!(data.as_ptr(), payload.slice(range).as_ptr()); - } - msg => panic!("unexpected message: {msg:?}"), - } - } - assert!(receiver.try_recv().is_err()); - } - - #[tokio::test] - async fn extended_data_bytes_preserves_extension_code() { - let payload = Bytes::from_static(b"stderr"); - let (write_half, mut receiver) = test_write_half(WindowSizeRef::new(1024), 1024); - - write_half - .extended_data_bytes(1, payload.clone()) - .await - .unwrap(); - - match receiver.recv().await.unwrap() { - (ChannelId(7), ChannelMsg::ExtendedData { data, ext }) => { - assert_eq!(ext, 1); - assert_eq!(data, payload); - assert_eq!(data.as_ptr(), payload.as_ptr()); - } - msg => panic!("unexpected message: {msg:?}"), - } - } - - #[tokio::test] - async fn data_bytes_empty_payload_sends_nothing() { - let (write_half, mut receiver) = test_write_half(WindowSizeRef::new(1024), 1024); - - write_half.data_bytes(Bytes::new()).await.unwrap(); - - assert!(receiver.try_recv().is_err()); - } - - #[tokio::test] - async fn data_bytes_waits_for_window_update() { - let window_size = WindowSizeRef::new(0); - let (write_half, mut receiver) = test_write_half(window_size.clone(), 1024); - let send = tokio::spawn(async move { - write_half - .data_bytes(Bytes::from_static(b"after-window")) - .await - .unwrap(); - }); - - tokio::task::yield_now().await; - assert!(!send.is_finished()); - - window_size.update(1024).await; - send.await.unwrap(); - - match receiver.recv().await.unwrap() { - (ChannelId(7), ChannelMsg::Data { data }) => { - assert_eq!(data.as_ref(), b"after-window"); - } - msg => panic!("unexpected message: {msg:?}"), - } - } - - #[tokio::test] - async fn data_bytes_rejects_zero_max_packet_size() { - let (write_half, mut receiver) = test_write_half(WindowSizeRef::new(1024), 0); - - let result = write_half.data_bytes(Bytes::from_static(b"owned")).await; - - assert!(matches!(result, Err(Error::Inconsistent))); - assert!(receiver.try_recv().is_err()); - } - - #[tokio::test] - async fn channel_data_bytes_forwards_to_write_half() { - let (sender, mut receiver) = mpsc::channel(8); - let (channel, _reference) = - Channel::<(ChannelId, ChannelMsg)>::new(ChannelId(9), sender, 1024, 1024, 8); - - channel.data_bytes(Bytes::from_static(b"channel")).await.unwrap(); - - match receiver.recv().await.unwrap() { - (ChannelId(9), ChannelMsg::Data { data }) => { - assert_eq!(data.as_ref(), b"channel"); - } - msg => panic!("unexpected message: {msg:?}"), - } - } -} diff --git a/crates/bssh-russh/src/cipher/benchmark.rs b/crates/bssh-russh/src/cipher/benchmark.rs deleted file mode 100644 index 85511cb0..00000000 --- a/crates/bssh-russh/src/cipher/benchmark.rs +++ /dev/null @@ -1,48 +0,0 @@ -#![allow(clippy::unwrap_used)] -use criterion::*; -use rand_core::TryRng; -use std::hint; - -pub fn bench(c: &mut Criterion) { - let mut rand_generator = hint::black_box(rand::rng()); - - let mut packet_length = hint::black_box(vec![0u8; 4]); - - for cipher_name in [super::CHACHA20_POLY1305, super::AES_256_GCM] { - let cipher = super::CIPHERS.get(&cipher_name).unwrap(); - - let mut key = vec![0; cipher.key_len()]; - rand_generator.try_fill_bytes(&mut key).unwrap(); - let mut nonce = vec![0; cipher.nonce_len()]; - rand_generator.try_fill_bytes(&mut nonce).unwrap(); - - let mut sk = cipher.make_sealing_key(&key, &nonce, &[], &crate::mac::_NONE); - let mut ok = cipher.make_opening_key(&key, &nonce, &[], &crate::mac::_NONE); - - let mut group = c.benchmark_group(format!("Cipher: {}", cipher_name.0)); - for size in [100usize, 1000, 10000] { - let iterations = 10000 / size; - - group.throughput(Throughput::Bytes(size as u64)); - group.bench_function(format!("Block size: {size}"), |b| { - b.iter_with_setup( - || { - let mut in_out = hint::black_box(vec![0u8; size]); - rand_generator.try_fill_bytes(&mut in_out).unwrap(); - rand_generator.try_fill_bytes(&mut packet_length).unwrap(); - in_out - }, - |mut in_out| { - for _ in 0..iterations { - let len = in_out.len(); - let (data, tag) = in_out.split_at_mut(len - sk.tag_len()); - sk.seal(0, data, tag); - ok.open(0, &mut in_out).unwrap(); - } - }, - ); - }); - } - group.finish(); - } -} diff --git a/crates/bssh-russh/src/cipher/block.rs b/crates/bssh-russh/src/cipher/block.rs deleted file mode 100644 index 8618978d..00000000 --- a/crates/bssh-russh/src/cipher/block.rs +++ /dev/null @@ -1,409 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::convert::TryInto; -use std::marker::PhantomData; - -use aes::cipher::{ - InOutBuf, Iv, IvSizeUser, Key, KeyIvInit, KeySizeUser, StreamCipher, StreamCipherError, - StreamCipherSeek, -}; -#[allow(deprecated)] -use rand_core::Rng; - -use super::super::Error; -use super::PACKET_LENGTH_LEN; -use crate::keys::key::safe_rng; -use crate::mac::{Mac, MacAlgorithm}; - -fn new_cipher_from_slices(k: &[u8], n: &[u8]) -> C { - #[allow(clippy::expect_used)] - C::new( - <&Key>::try_from(k).expect("key length matches"), - <&Iv>::try_from(n).expect("iv length matches"), - ) -} - -/// Cloneable wrapper for `Ctr128BE<>` -pub struct CtrWrapper -where - C: KeyIvInit, -{ - key: Key, - initial_iv: Iv, - pos: u64, -} - -impl Clone for CtrWrapper { - fn clone(&self) -> Self { - Self { - key: self.key.clone(), - initial_iv: self.initial_iv.clone(), - pos: self.pos, - } - } -} - -impl KeySizeUser for CtrWrapper { - type KeySize = ::KeySize; -} - -impl IvSizeUser for CtrWrapper { - type IvSize = ::IvSize; -} - -impl KeyIvInit for CtrWrapper { - fn new(key: &Key, iv: &Iv) -> Self { - Self { - key: key.clone(), - initial_iv: iv.clone(), - pos: 0, - } - } -} - -impl StreamCipher for CtrWrapper { - fn check_remaining(&self, _data_len: usize) -> Result<(), StreamCipherError> { - Ok(()) - } - - fn unchecked_apply_keystream_inout(&mut self, buf: InOutBuf<'_, '_, u8>) { - let mut cipher = C::new(&self.key, &self.initial_iv); - cipher.seek(self.pos); - cipher.unchecked_apply_keystream_inout(buf); - self.pos = cipher.current_pos(); - } - - fn unchecked_write_keystream(&mut self, buf: &mut [u8]) { - let mut cipher = C::new(&self.key, &self.initial_iv); - cipher.seek(self.pos); - cipher.unchecked_write_keystream(buf); - self.pos = cipher.current_pos(); - } -} - -pub struct SshBlockCipher( - pub PhantomData, -); - -impl< - C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static, -> super::Cipher for SshBlockCipher -{ - fn key_len(&self) -> usize { - C::key_size() - } - - fn nonce_len(&self) -> usize { - C::iv_size() - } - - fn needs_mac(&self) -> bool { - true - } - - fn make_opening_key( - &self, - k: &[u8], - n: &[u8], - m: &[u8], - mac: &dyn MacAlgorithm, - ) -> Box { - Box::new(OpeningKey { - cipher: new_cipher_from_slices::(k, n), - mac: mac.make_mac(m), - }) - } - - fn make_sealing_key( - &self, - k: &[u8], - n: &[u8], - m: &[u8], - mac: &dyn MacAlgorithm, - ) -> Box { - Box::new(SealingKey { - cipher: new_cipher_from_slices::(k, n), - mac: mac.make_mac(m), - }) - } -} - -pub struct OpeningKey { - pub(crate) cipher: C, - pub(crate) mac: Box, -} - -pub struct SealingKey { - pub(crate) cipher: C, - pub(crate) mac: Box, -} - -impl super::OpeningKey - for OpeningKey -{ - fn packet_length_to_read_for_block_length(&self) -> usize { - 16 - } - - fn decrypt_packet_length( - &self, - _sequence_number: u32, - encrypted_packet_length: &[u8], - ) -> [u8; 4] { - let mut first_block = [0u8; 16]; - // Fine because of self.packet_length_to_read_for_block_length() - #[allow(clippy::indexing_slicing)] - first_block.copy_from_slice(&encrypted_packet_length[..16]); - - if self.mac.is_etm() { - // Fine because of self.packet_length_to_read_for_block_length() - #[allow(clippy::unwrap_used, clippy::indexing_slicing)] - encrypted_packet_length[..4].try_into().unwrap() - } else { - self.cipher.decrypt_packet_length_block(&mut first_block); - - // Fine because of self.packet_length_to_read_for_block_length() - #[allow(clippy::unwrap_used, clippy::indexing_slicing)] - first_block[..4].try_into().unwrap() - } - } - - fn tag_len(&self) -> usize { - self.mac.mac_len() - } - - fn open<'a>( - &mut self, - sequence_number: u32, - ciphertext_and_tag: &'a mut [u8], - ) -> Result<&'a [u8], Error> { - let ciphertext_len = ciphertext_and_tag.len() - self.tag_len(); - let (ciphertext_in_plaintext_out, tag) = ciphertext_and_tag.split_at_mut(ciphertext_len); - if self.mac.is_etm() { - if !self - .mac - .verify(sequence_number, ciphertext_in_plaintext_out, tag) - { - return Err(Error::PacketAuth); - } - #[allow(clippy::indexing_slicing)] - self.cipher - .decrypt_data(&mut ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]); - } else { - self.cipher.decrypt_data(ciphertext_in_plaintext_out); - - if !self - .mac - .verify(sequence_number, ciphertext_in_plaintext_out, tag) - { - return Err(Error::PacketAuth); - } - } - - #[allow(clippy::indexing_slicing)] - Ok(&ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]) - } -} - -impl super::SealingKey for SealingKey { - fn padding_length(&self, payload: &[u8]) -> usize { - let block_size = 16; - - let pll = if self.mac.is_etm() { - 0 - } else { - PACKET_LENGTH_LEN - }; - - let extra_len = PACKET_LENGTH_LEN + super::PADDING_LENGTH_LEN + self.mac.mac_len(); - - let padding_len = if payload.len() + extra_len <= super::MINIMUM_PACKET_LEN { - super::MINIMUM_PACKET_LEN - payload.len() - super::PADDING_LENGTH_LEN - pll - } else { - block_size - ((pll + super::PADDING_LENGTH_LEN + payload.len()) % block_size) - }; - if padding_len < PACKET_LENGTH_LEN { - padding_len + block_size - } else { - padding_len - } - } - - fn fill_padding(&self, padding_out: &mut [u8]) { - safe_rng().fill_bytes(padding_out); - } - - fn tag_len(&self) -> usize { - self.mac.mac_len() - } - - fn seal( - &mut self, - sequence_number: u32, - plaintext_in_ciphertext_out: &mut [u8], - tag_out: &mut [u8], - ) { - if self.mac.is_etm() { - #[allow(clippy::indexing_slicing)] - self.cipher - .encrypt_data(&mut plaintext_in_ciphertext_out[PACKET_LENGTH_LEN..]); - self.mac - .compute(sequence_number, plaintext_in_ciphertext_out, tag_out); - } else { - self.mac - .compute(sequence_number, plaintext_in_ciphertext_out, tag_out); - self.cipher.encrypt_data(plaintext_in_ciphertext_out); - } - } -} - -pub trait BlockStreamCipher { - fn encrypt_data(&mut self, data: &mut [u8]); - fn decrypt_data(&mut self, data: &mut [u8]); -} - -pub(crate) trait PacketLengthProbe { - fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]); -} - -impl BlockStreamCipher for T { - fn encrypt_data(&mut self, data: &mut [u8]) { - self.apply_keystream(data); - } - - fn decrypt_data(&mut self, data: &mut [u8]) { - self.apply_keystream(data); - } -} - -impl PacketLengthProbe for T { - fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) { - let mut cipher = self.clone(); - cipher.apply_keystream(first_block); - } -} - -#[cfg(test)] -mod tests { - use aes::Aes128; - use aes::cipher::KeyIvInit; - use aes::cipher::StreamCipher; - use aes::cipher::{IvSizeUser, KeySizeUser}; - use ctr::Ctr128BE; - use digest::typenum::U16; - use tokio::io::AsyncWriteExt; - - use super::{BlockStreamCipher, CtrWrapper, OpeningKey, PacketLengthProbe}; - use crate::mac::MacAlgorithm; - use crate::sshbuffer::SSHBuffer; - - #[test] - fn stream_cipher_probe_does_not_advance_cipher_state() { - let plaintext = *b"0123456789ABCDEF"; - let key = fixture_bytes::<16>(7); - let iv = fixture_bytes::<16>(3); - - let mut encryptor = CtrWrapper::>::new(&key.into(), &iv.into()); - let mut ciphertext = plaintext; - encryptor.apply_keystream(&mut ciphertext); - - let cipher = CtrWrapper::>::new(&key.into(), &iv.into()); - let mut probed_block = ciphertext; - cipher.decrypt_packet_length_block(&mut probed_block); - assert_eq!(probed_block, plaintext); - - let mut decrypted = ciphertext; - let mut cipher_after_probe = cipher; - cipher_after_probe.decrypt_data(&mut decrypted); - assert_eq!(decrypted, plaintext); - } - - #[test] - fn decrypt_packet_length_uses_independent_cipher_state() -> std::io::Result<()> { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let opening = OpeningKey { - cipher: OwnedStateCipher::new(), - mac: crate::mac::_NONE.make_mac(&[]), - }; - let mut opening = opening; - let mut buffer = SSHBuffer::new(); - let bytes_read = runtime - .block_on(async { - let (mut writer, mut reader) = tokio::io::duplex(64); - writer.write_all(&[0; 17]).await?; - drop(writer); - crate::cipher::read(&mut reader, &mut buffer, &mut opening).await - }) - .map_err(std::io::Error::other)?; - - assert_eq!(bytes_read, 16); - Ok(()) - } - - struct OwnedStateCipher { - packet_length: Box<[u8; 4]>, - } - - impl OwnedStateCipher { - fn new() -> Self { - Self { - packet_length: Box::new([0, 0, 0, 13]), - } - } - } - - impl Clone for OwnedStateCipher { - fn clone(&self) -> Self { - Self { - packet_length: Box::new([0, 0, 0, 12]), - } - } - } - - impl KeySizeUser for OwnedStateCipher { - type KeySize = U16; - } - - impl IvSizeUser for OwnedStateCipher { - type IvSize = U16; - } - - impl BlockStreamCipher for OwnedStateCipher { - fn encrypt_data(&mut self, _data: &mut [u8]) {} - - fn decrypt_data(&mut self, data: &mut [u8]) { - if let Some(prefix) = data.get_mut(..4) { - prefix.copy_from_slice(&self.packet_length[..]); - } - } - } - - impl PacketLengthProbe for OwnedStateCipher { - fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) { - if let Some(prefix) = first_block.get_mut(..4) { - prefix.copy_from_slice(&[0, 0, 0, 12]); - } - } - } - - fn fixture_bytes(seed: u8) -> [u8; N] { - let mut bytes = [0; N]; - for (i, byte) in bytes.iter_mut().enumerate() { - *byte = seed.wrapping_add(i as u8); - } - bytes - } -} diff --git a/crates/bssh-russh/src/cipher/cbc.rs b/crates/bssh-russh/src/cipher/cbc.rs deleted file mode 100644 index 100c93bd..00000000 --- a/crates/bssh-russh/src/cipher/cbc.rs +++ /dev/null @@ -1,137 +0,0 @@ -use cbc::cipher::{InnerIvInit, Iv, IvSizeUser}; -use cbc::{Decryptor, Encryptor}; -use cipher::common::InnerUser; -use cipher::{ - Block, BlockCipherDecrypt, BlockCipherEncrypt, BlockModeDecrypt, BlockModeEncrypt, IvState, -}; - -use super::block::{BlockStreamCipher, PacketLengthProbe}; - -/// CBC wrapper that stores the decryption cipher and IV separately rather than -/// a `cbc::Decryptor`, because `Decryptor` is no longer `Clone` in cbc 0.2. -/// This allows stateless peeking at the packet length block without cloning. -pub struct CbcWrapper -where - C: BlockCipherEncrypt + BlockCipherDecrypt, -{ - encryptor: Encryptor, - /// Raw cipher used for decryption. `BlockCipherDecrypt::decrypt_block` takes - /// `&self`, so this can be used without mutation for packet-length probing. - dec_cipher: C, - /// Current CBC decryption IV (i.e. the last ciphertext block consumed). - dec_iv: Block, -} - -impl CbcWrapper -where - C: BlockCipherEncrypt + BlockCipherDecrypt + Clone, -{ - #[must_use] - fn decrypt_inner(&self, data: &mut [u8]) -> Iv { - let mut dec = Decryptor::<&C>::inner_iv_init(&self.dec_cipher, &self.dec_iv); - - for chunk in data.chunks_exact_mut(C::block_size()) { - #[allow(clippy::expect_used)] - let block = <&mut Block>::try_from(chunk).expect("chunk length matches block size"); - - dec.decrypt_block(block); - } - - dec.iv_state() - } -} - -impl InnerUser for CbcWrapper { - type Inner = C; -} - -impl IvSizeUser for CbcWrapper { - type IvSize = C::BlockSize; -} - -impl BlockStreamCipher for CbcWrapper { - fn encrypt_data(&mut self, data: &mut [u8]) { - for chunk in data.chunks_exact_mut(C::block_size()) { - #[allow(clippy::expect_used)] - let block = <&mut Block>::try_from(chunk).expect("chunk length matches block size"); - self.encryptor.encrypt_block(block); - } - } - - fn decrypt_data(&mut self, data: &mut [u8]) { - self.dec_iv = self.decrypt_inner(data) - } -} - -impl PacketLengthProbe for CbcWrapper { - fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) { - let _ = self.decrypt_inner(first_block); - } -} - -impl InnerIvInit for CbcWrapper { - #[inline] - fn inner_iv_init(cipher: C, iv: &Iv) -> Self { - Self { - encryptor: Encryptor::inner_iv_init(cipher.clone(), iv), - dec_cipher: cipher, - dec_iv: iv.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use aes::Aes128; - use cbc::cipher::KeyIvInit; - #[cfg(feature = "des")] - use des::TdesEde3; - - use super::{BlockStreamCipher, CbcWrapper, PacketLengthProbe}; - - #[test] - fn packet_length_probe_does_not_advance_cbc_decryptor_state() { - let plaintext = *b"0123456789ABCDEF"; - let key = fixture_bytes::<16>(11); - let iv = fixture_bytes::<16>(5); - - let mut encryptor = CbcWrapper::::new(&key.into(), &iv.into()); - let mut ciphertext = plaintext; - encryptor.encrypt_data(&mut ciphertext); - - let cipher = CbcWrapper::::new(&key.into(), &iv.into()); - let mut probed_block = ciphertext; - cipher.decrypt_packet_length_block(&mut probed_block); - assert_eq!(probed_block, plaintext); - - let mut decrypted = ciphertext; - let mut cipher_after_probe = cipher; - cipher_after_probe.decrypt_data(&mut decrypted); - assert_eq!(decrypted, plaintext); - } - - #[cfg(feature = "des")] - #[test] - fn packet_length_probe_respects_3des_block_size() { - let plaintext = *b"0123456789ABCDEF"; - let key = fixture_bytes::<24>(11); - let iv = fixture_bytes::<8>(5); - - let mut encryptor = CbcWrapper::::new(&key.into(), &iv.into()); - let mut ciphertext = plaintext; - encryptor.encrypt_data(&mut ciphertext); - - let cipher = CbcWrapper::::new(&key.into(), &iv.into()); - let mut probed_block = ciphertext; - cipher.decrypt_packet_length_block(&mut probed_block); - assert_eq!(probed_block, plaintext); - } - - fn fixture_bytes(seed: u8) -> [u8; N] { - let mut bytes = [0; N]; - for (i, byte) in bytes.iter_mut().enumerate() { - *byte = seed.wrapping_add(i as u8); - } - bytes - } -} diff --git a/crates/bssh-russh/src/cipher/chacha20poly1305.rs b/crates/bssh-russh/src/cipher/chacha20poly1305.rs deleted file mode 100644 index 8e288b73..00000000 --- a/crates/bssh-russh/src/cipher/chacha20poly1305.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD - -#[cfg(feature = "aws-lc-rs")] -use aws_lc_rs::aead::chacha20_poly1305_openssh; -#[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))] -use ring::aead::chacha20_poly1305_openssh; - -use super::super::Error; -use crate::mac::MacAlgorithm; - -pub struct SshChacha20Poly1305Cipher {} - -impl super::Cipher for SshChacha20Poly1305Cipher { - fn key_len(&self) -> usize { - chacha20_poly1305_openssh::KEY_LEN - } - - fn make_opening_key( - &self, - k: &[u8], - _: &[u8], - _: &[u8], - _: &dyn MacAlgorithm, - ) -> Box { - Box::new(OpeningKey(chacha20_poly1305_openssh::OpeningKey::new( - #[allow(clippy::unwrap_used)] - k.try_into().unwrap(), - ))) - } - - fn make_sealing_key( - &self, - k: &[u8], - _: &[u8], - _: &[u8], - _: &dyn MacAlgorithm, - ) -> Box { - Box::new(SealingKey(chacha20_poly1305_openssh::SealingKey::new( - #[allow(clippy::unwrap_used)] - k.try_into().unwrap(), - ))) - } -} - -pub struct OpeningKey(chacha20_poly1305_openssh::OpeningKey); - -pub struct SealingKey(chacha20_poly1305_openssh::SealingKey); - -impl super::OpeningKey for OpeningKey { - fn decrypt_packet_length( - &self, - sequence_number: u32, - encrypted_packet_length: &[u8], - ) -> [u8; 4] { - self.0.decrypt_packet_length( - sequence_number, - #[allow(clippy::unwrap_used)] - encrypted_packet_length.try_into().unwrap(), - ) - } - - fn tag_len(&self) -> usize { - chacha20_poly1305_openssh::TAG_LEN - } - - fn open<'a>( - &mut self, - sequence_number: u32, - ciphertext_and_tag: &'a mut [u8], - ) -> Result<&'a [u8], Error> { - let ciphertext_len = ciphertext_and_tag.len() - self.tag_len(); - let (ciphertext_in_plaintext_out, tag) = ciphertext_and_tag.split_at_mut(ciphertext_len); - - self.0 - .open_in_place( - sequence_number, - ciphertext_in_plaintext_out, - #[allow(clippy::unwrap_used)] - &tag.try_into().unwrap(), - ) - .map_err(|_| Error::DecryptionError) - } -} - -impl super::SealingKey for SealingKey { - fn padding_length(&self, payload: &[u8]) -> usize { - let block_size = 8; - let extra_len = super::PACKET_LENGTH_LEN + super::PADDING_LENGTH_LEN; - let padding_len = if payload.len() + extra_len <= super::MINIMUM_PACKET_LEN { - super::MINIMUM_PACKET_LEN - payload.len() - super::PADDING_LENGTH_LEN - } else { - block_size - ((super::PADDING_LENGTH_LEN + payload.len()) % block_size) - }; - if padding_len < super::PACKET_LENGTH_LEN { - padding_len + block_size - } else { - padding_len - } - } - - // As explained in "SSH via CTR mode with stateful decryption" in - // https://openvpn.net/papers/ssh-security.pdf, the padding doesn't need to - // be random because we're doing stateful counter-mode encryption. Use - // fixed padding to avoid PRNG overhead. - fn fill_padding(&self, padding_out: &mut [u8]) { - for padding_byte in padding_out { - *padding_byte = 0; - } - } - - fn tag_len(&self) -> usize { - chacha20_poly1305_openssh::TAG_LEN - } - - fn seal( - &mut self, - sequence_number: u32, - plaintext_in_ciphertext_out: &mut [u8], - tag: &mut [u8], - ) { - self.0.seal_in_place( - sequence_number, - plaintext_in_ciphertext_out, - #[allow(clippy::unwrap_used)] - tag.try_into().unwrap(), - ); - } -} diff --git a/crates/bssh-russh/src/cipher/clear.rs b/crates/bssh-russh/src/cipher/clear.rs deleted file mode 100644 index 955a4e80..00000000 --- a/crates/bssh-russh/src/cipher/clear.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::convert::TryInto; - -use crate::mac::MacAlgorithm; -use crate::Error; - -#[derive(Debug)] -pub struct Key; - -pub struct Clear {} - -impl super::Cipher for Clear { - fn key_len(&self) -> usize { - 0 - } - - fn make_opening_key( - &self, - _: &[u8], - _: &[u8], - _: &[u8], - _: &dyn MacAlgorithm, - ) -> Box { - Box::new(Key {}) - } - - fn make_sealing_key( - &self, - _: &[u8], - _: &[u8], - _: &[u8], - _: &dyn MacAlgorithm, - ) -> Box { - Box::new(Key {}) - } -} - -impl super::OpeningKey for Key { - fn decrypt_packet_length(&self, _seqn: u32, packet_length: &[u8]) -> [u8; 4] { - // Fine because of self.packet_length_to_read_for_block_length() - #[allow(clippy::unwrap_used, clippy::indexing_slicing)] - packet_length.try_into().unwrap() - } - - fn tag_len(&self) -> usize { - 0 - } - - fn open<'a>( - &mut self, - _seqn: u32, - ciphertext_and_tag: &'a mut [u8], - ) -> Result<&'a [u8], Error> { - #[allow(clippy::indexing_slicing)] // length known - Ok(&ciphertext_and_tag[4..]) - } -} - -impl super::SealingKey for Key { - // Cleartext packets (including lengths) must be multiple of 8 in - // length. - fn padding_length(&self, payload: &[u8]) -> usize { - let block_size = 8; - let padding_len = block_size - ((5 + payload.len()) % block_size); - if padding_len < 4 { - padding_len + block_size - } else { - padding_len - } - } - - fn fill_padding(&self, padding_out: &mut [u8]) { - // Since the packet is unencrypted anyway, there's no advantage to - // randomizing the padding, so avoid possibly leaking extra RNG state - // by padding with zeros. - for padding_byte in padding_out { - *padding_byte = 0; - } - } - - fn tag_len(&self) -> usize { - 0 - } - - fn seal(&mut self, _seqn: u32, _plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]) { - debug_assert_eq!(tag_out.len(), self.tag_len()); - } -} diff --git a/crates/bssh-russh/src/cipher/gcm.rs b/crates/bssh-russh/src/cipher/gcm.rs deleted file mode 100644 index 9afbdbb3..00000000 --- a/crates/bssh-russh/src/cipher/gcm.rs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD - -use std::convert::TryInto; - -#[cfg(feature = "aws-lc-rs")] -use aws_lc_rs::{ - aead::{ - Aad, Algorithm, BoundKey, NONCE_LEN, Nonce as AeadNonce, NonceSequence, - OpeningKey as AeadOpeningKey, SealingKey as AeadSealingKey, UnboundKey, - }, - error::Unspecified, -}; -use rand_core::Rng; -#[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))] -use ring::{ - aead::{ - Aad, Algorithm, BoundKey, NONCE_LEN, Nonce as AeadNonce, NonceSequence, - OpeningKey as AeadOpeningKey, SealingKey as AeadSealingKey, UnboundKey, - }, - error::Unspecified, -}; - -use super::super::Error; -use crate::keys::key::safe_rng; -use crate::mac::MacAlgorithm; - -pub struct GcmCipher(pub(crate) &'static Algorithm); - -impl super::Cipher for GcmCipher { - fn key_len(&self) -> usize { - self.0.key_len() - } - - fn nonce_len(&self) -> usize { - self.0.nonce_len() - } - - fn make_opening_key( - &self, - k: &[u8], - n: &[u8], - _: &[u8], - _: &dyn MacAlgorithm, - ) -> Box { - #[allow(clippy::unwrap_used)] - Box::new(OpeningKey(AeadOpeningKey::new( - UnboundKey::new(self.0, k).unwrap(), - Nonce(n.try_into().unwrap()), - ))) - } - - fn make_sealing_key( - &self, - k: &[u8], - n: &[u8], - _: &[u8], - _: &dyn MacAlgorithm, - ) -> Box { - #[allow(clippy::unwrap_used)] - Box::new(SealingKey(AeadSealingKey::new( - UnboundKey::new(self.0, k).unwrap(), - Nonce(n.try_into().unwrap()), - ))) - } -} - -pub struct OpeningKey(AeadOpeningKey); - -pub struct SealingKey(AeadSealingKey); - -struct Nonce([u8; NONCE_LEN]); - -impl NonceSequence for Nonce { - fn advance(&mut self) -> Result { - let mut previous_nonce = [0u8; NONCE_LEN]; - #[allow(clippy::indexing_slicing)] // length checked - previous_nonce.clone_from_slice(&self.0[..]); - let mut carry = 1; - #[allow(clippy::indexing_slicing)] // length checked - for i in (0..NONCE_LEN).rev() { - let n = self.0[i] as u16 + carry; - self.0[i] = n as u8; - carry = n >> 8; - } - Ok(AeadNonce::assume_unique_for_key(previous_nonce)) - } -} - -impl super::OpeningKey for OpeningKey { - fn decrypt_packet_length( - &self, - _sequence_number: u32, - encrypted_packet_length: &[u8], - ) -> [u8; 4] { - // Fine because of self.packet_length_to_read_for_block_length() - #[allow(clippy::unwrap_used, clippy::indexing_slicing)] - encrypted_packet_length.try_into().unwrap() - } - - fn tag_len(&self) -> usize { - self.0.algorithm().tag_len() - } - - fn open<'a>( - &mut self, - _sequence_number: u32, - ciphertext_and_tag: &'a mut [u8], - ) -> Result<&'a [u8], Error> { - // Packet length is sent unencrypted - let mut packet_length = [0; super::PACKET_LENGTH_LEN]; - - #[allow(clippy::indexing_slicing)] // length checked - packet_length.clone_from_slice(&ciphertext_and_tag[..super::PACKET_LENGTH_LEN]); - - let buf = self - .0 - .open_in_place( - Aad::from(&packet_length), - #[allow(clippy::indexing_slicing)] // length checked - &mut ciphertext_and_tag[super::PACKET_LENGTH_LEN..], - ) - .map_err(|_| Error::DecryptionError)?; - - Ok(buf) - } -} - -impl super::SealingKey for SealingKey { - fn padding_length(&self, payload: &[u8]) -> usize { - let block_size = 16; - let extra_len = super::PACKET_LENGTH_LEN + super::PADDING_LENGTH_LEN; - let padding_len = if payload.len() + extra_len <= super::MINIMUM_PACKET_LEN { - super::MINIMUM_PACKET_LEN - payload.len() - super::PADDING_LENGTH_LEN - } else { - block_size - ((super::PADDING_LENGTH_LEN + payload.len()) % block_size) - }; - if padding_len < super::PACKET_LENGTH_LEN { - padding_len + block_size - } else { - padding_len - } - } - - fn fill_padding(&self, padding_out: &mut [u8]) { - safe_rng().fill_bytes(padding_out); - } - - fn tag_len(&self) -> usize { - self.0.algorithm().tag_len() - } - - fn seal( - &mut self, - _sequence_number: u32, - plaintext_in_ciphertext_out: &mut [u8], - tag: &mut [u8], - ) { - // Packet length is received unencrypted - let mut packet_length = [0; super::PACKET_LENGTH_LEN]; - #[allow(clippy::indexing_slicing)] // length checked - packet_length.clone_from_slice(&plaintext_in_ciphertext_out[..super::PACKET_LENGTH_LEN]); - - #[allow(clippy::unwrap_used)] - let tag_out = self - .0 - .seal_in_place_separate_tag( - Aad::from(&packet_length), - #[allow(clippy::indexing_slicing)] - &mut plaintext_in_ciphertext_out[super::PACKET_LENGTH_LEN..], - ) - .unwrap(); - - tag.clone_from_slice(tag_out.as_ref()); - } -} diff --git a/crates/bssh-russh/src/cipher/mod.rs b/crates/bssh-russh/src/cipher/mod.rs deleted file mode 100644 index 7250d608..00000000 --- a/crates/bssh-russh/src/cipher/mod.rs +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! -//! This module exports cipher names for use with [Preferred]. -use std::borrow::Borrow; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::fmt::Debug; -use std::marker::PhantomData; -use std::num::Wrapping; -use std::sync::LazyLock; - -use aes::{Aes128, Aes192, Aes256}; -#[cfg(feature = "aws-lc-rs")] -use aws_lc_rs::aead::{AES_128_GCM as ALGORITHM_AES_128_GCM, AES_256_GCM as ALGORITHM_AES_256_GCM}; -use byteorder::{BigEndian, ByteOrder}; -use block::CtrWrapper; -use ctr::Ctr128BE; -use delegate::delegate; -use log::trace; -#[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))] -use ring::aead::{AES_128_GCM as ALGORITHM_AES_128_GCM, AES_256_GCM as ALGORITHM_AES_256_GCM}; -use ssh_encoding::Encode; -use tokio::io::{AsyncRead, AsyncReadExt}; - -use self::cbc::CbcWrapper; -use crate::Error; -use crate::mac::MacAlgorithm; -use crate::sshbuffer::SSHBuffer; - -pub(crate) mod block; -pub(crate) mod cbc; -pub(crate) mod chacha20poly1305; -pub(crate) mod clear; -pub(crate) mod gcm; - -use block::SshBlockCipher; -use chacha20poly1305::SshChacha20Poly1305Cipher; -use clear::Clear; -use gcm::GcmCipher; - -pub(crate) trait Cipher { - fn needs_mac(&self) -> bool { - false - } - fn key_len(&self) -> usize; - fn nonce_len(&self) -> usize { - 0 - } - fn make_opening_key( - &self, - key: &[u8], - nonce: &[u8], - mac_key: &[u8], - mac: &dyn MacAlgorithm, - ) -> Box; - fn make_sealing_key( - &self, - key: &[u8], - nonce: &[u8], - mac_key: &[u8], - mac: &dyn MacAlgorithm, - ) -> Box; -} - -/// `clear` -pub const CLEAR: Name = Name("clear"); -/// `3des-cbc` -#[cfg(feature = "des")] -pub const TRIPLE_DES_CBC: Name = Name("3des-cbc"); -/// `aes128-ctr` -pub const AES_128_CTR: Name = Name("aes128-ctr"); -/// `aes192-ctr` -pub const AES_192_CTR: Name = Name("aes192-ctr"); -/// `aes128-cbc` -pub const AES_128_CBC: Name = Name("aes128-cbc"); -/// `aes192-cbc` -pub const AES_192_CBC: Name = Name("aes192-cbc"); -/// `aes256-cbc` -pub const AES_256_CBC: Name = Name("aes256-cbc"); -/// `aes256-ctr` -pub const AES_256_CTR: Name = Name("aes256-ctr"); -/// `aes128-gcm@openssh.com` -pub const AES_128_GCM: Name = Name("aes128-gcm@openssh.com"); -/// `aes256-gcm@openssh.com` -pub const AES_256_GCM: Name = Name("aes256-gcm@openssh.com"); -/// `chacha20-poly1305@openssh.com` -pub const CHACHA20_POLY1305: Name = Name("chacha20-poly1305@openssh.com"); -/// `none` -pub const NONE: Name = Name("none"); - -pub(crate) static _CLEAR: Clear = Clear {}; -#[cfg(feature = "des")] -static _3DES_CBC: SshBlockCipher> = SshBlockCipher(PhantomData); -static _AES_128_CTR: SshBlockCipher>> = SshBlockCipher(PhantomData); -static _AES_192_CTR: SshBlockCipher>> = SshBlockCipher(PhantomData); -static _AES_256_CTR: SshBlockCipher>> = SshBlockCipher(PhantomData); -static _AES_128_GCM: GcmCipher = GcmCipher(&ALGORITHM_AES_128_GCM); -static _AES_256_GCM: GcmCipher = GcmCipher(&ALGORITHM_AES_256_GCM); -static _AES_128_CBC: SshBlockCipher> = SshBlockCipher(PhantomData); -static _AES_192_CBC: SshBlockCipher> = SshBlockCipher(PhantomData); -static _AES_256_CBC: SshBlockCipher> = SshBlockCipher(PhantomData); -static _CHACHA20_POLY1305: SshChacha20Poly1305Cipher = SshChacha20Poly1305Cipher {}; - -pub static ALL_CIPHERS: &[&Name] = &[ - &CLEAR, - &NONE, - #[cfg(feature = "des")] - &TRIPLE_DES_CBC, - &AES_128_CTR, - &AES_192_CTR, - &AES_256_CTR, - &AES_128_GCM, - &AES_256_GCM, - &AES_128_CBC, - &AES_192_CBC, - &AES_256_CBC, - &CHACHA20_POLY1305, -]; - -pub(crate) static CIPHERS: LazyLock> = - LazyLock::new(|| { - let mut h: HashMap<&'static Name, &(dyn Cipher + Send + Sync)> = HashMap::new(); - h.insert(&CLEAR, &_CLEAR); - h.insert(&NONE, &_CLEAR); - #[cfg(feature = "des")] - h.insert(&TRIPLE_DES_CBC, &_3DES_CBC); - h.insert(&AES_128_CTR, &_AES_128_CTR); - h.insert(&AES_192_CTR, &_AES_192_CTR); - h.insert(&AES_256_CTR, &_AES_256_CTR); - h.insert(&AES_128_GCM, &_AES_128_GCM); - h.insert(&AES_256_GCM, &_AES_256_GCM); - h.insert(&AES_128_CBC, &_AES_128_CBC); - h.insert(&AES_192_CBC, &_AES_192_CBC); - h.insert(&AES_256_CBC, &_AES_256_CBC); - h.insert(&CHACHA20_POLY1305, &_CHACHA20_POLY1305); - assert_eq!(h.len(), ALL_CIPHERS.len()); - h - }); - -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub struct Name(&'static str); -impl AsRef for Name { - fn as_ref(&self) -> &str { - self.0 - } -} - -impl Encode for Name { - delegate! { to self.as_ref() { - fn encoded_len(&self) -> Result; - fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error>; - }} -} - -impl Borrow for &Name { - fn borrow(&self) -> &str { - self.0 - } -} - -impl TryFrom<&str> for Name { - type Error = (); - fn try_from(s: &str) -> Result { - CIPHERS.keys().find(|x| x.0 == s).map(|x| **x).ok_or(()) - } -} - -pub(crate) struct CipherPair { - pub local_to_remote: Box, - pub remote_to_local: Box, -} - -impl Debug for CipherPair { - fn fmt(&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - Ok(()) - } -} - -pub(crate) trait OpeningKey { - fn packet_length_to_read_for_block_length(&self) -> usize { - 4 - } - - fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: &[u8]) -> [u8; 4]; - - fn tag_len(&self) -> usize; - - fn open<'a>(&mut self, seqn: u32, ciphertext_and_tag: &'a mut [u8]) -> Result<&'a [u8], Error>; -} - -pub(crate) trait SealingKey { - fn padding_length(&self, plaintext: &[u8]) -> usize; - - fn fill_padding(&self, padding_out: &mut [u8]); - - fn tag_len(&self) -> usize; - - fn seal(&mut self, seqn: u32, plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]); - - #[allow(clippy::indexing_slicing)] // PacketWriter reserves and sizes the packet buffer first - fn finish_packet(&mut self, offset: usize, payload_len: usize, buffer: &mut SSHBuffer) { - let payload_start = offset + PACKET_LENGTH_LEN + PADDING_LENGTH_LEN; - let payload_end = payload_start + payload_len; - - trace!("writing, seqn = {:?}", buffer.seqn.0); - let padding_length = self.padding_length(&buffer.buffer[payload_start..payload_end]); - trace!("padding length {padding_length:?}"); - let packet_length = PADDING_LENGTH_LEN + payload_len + padding_length; - trace!("packet_length {packet_length:?}"); - - // Maximum packet length: - // https://tools.ietf.org/html/rfc4253#section-6.1 - assert!(packet_length <= u32::MAX as usize); - BigEndian::write_u32( - &mut buffer.buffer[offset..offset + PACKET_LENGTH_LEN], - packet_length as u32, - ); - - assert!(padding_length <= u8::MAX as usize); - buffer.buffer[offset + PACKET_LENGTH_LEN] = padding_length as u8; - buffer.buffer.resize(payload_end + padding_length, 0); - #[allow(clippy::indexing_slicing)] // length checked - self.fill_padding(&mut buffer.buffer[payload_end..]); - let tag_offset = buffer.buffer.len(); - buffer.buffer.resize(tag_offset + self.tag_len(), 0); - - #[allow(clippy::indexing_slicing)] // length checked - let (plaintext, tag) = - buffer.buffer[offset..].split_at_mut(PACKET_LENGTH_LEN + packet_length); - - self.seal(buffer.seqn.0, plaintext, tag); - - buffer.bytes += payload_len; - // Sequence numbers are on 32 bits and wrap. - // https://tools.ietf.org/html/rfc4253#section-6.4 - buffer.seqn += Wrapping(1); - } - - fn write(&mut self, payload: &[u8], buffer: &mut SSHBuffer) { - // https://tools.ietf.org/html/rfc4253#section-6 - // - // The variables `payload`, `packet_length` and `padding_length` refer - // to the protocol fields of the same names. - trace!("writing, seqn = {:?}", buffer.seqn.0); - - let padding_length = self.padding_length(payload); - trace!("padding length {padding_length:?}"); - let packet_length = PADDING_LENGTH_LEN + payload.len() + padding_length; - trace!("packet_length {packet_length:?}"); - let offset = buffer.buffer.len(); - - // Maximum packet length: - // https://tools.ietf.org/html/rfc4253#section-6.1 - assert!(packet_length <= u32::MAX as usize); - buffer - .buffer - .extend_from_slice(&(packet_length as u32).to_be_bytes()); - - assert!(padding_length <= u8::MAX as usize); - buffer.buffer.push(padding_length as u8); - buffer.buffer.extend_from_slice(payload); - let pad_offset = buffer.buffer.len(); - buffer.buffer.resize(pad_offset + padding_length, 0); - #[allow(clippy::indexing_slicing)] // length checked - self.fill_padding(&mut buffer.buffer[pad_offset..]); - let tag_offset = buffer.buffer.len(); - buffer.buffer.resize(tag_offset + self.tag_len(), 0); - - #[allow(clippy::indexing_slicing)] // length checked - let (plaintext, tag) = - buffer.buffer[offset..].split_at_mut(PACKET_LENGTH_LEN + packet_length); - - self.seal(buffer.seqn.0, plaintext, tag); - - buffer.bytes += payload.len(); - // Sequence numbers are on 32 bits and wrap. - // https://tools.ietf.org/html/rfc4253#section-6.4 - buffer.seqn += Wrapping(1); - } -} - -pub(crate) async fn read( - stream: &mut R, - buffer: &mut SSHBuffer, - cipher: &mut (dyn OpeningKey + Send), -) -> Result { - if buffer.len == 0 { - let mut len = vec![0; cipher.packet_length_to_read_for_block_length()]; - - stream.read_exact(&mut len).await?; - trace!("reading, len = {len:?}"); - { - let seqn = buffer.seqn.0; - buffer.buffer.clear(); - buffer.buffer.extend_from_slice(&len); - trace!("reading, seqn = {seqn:?}"); - let len = cipher.decrypt_packet_length(seqn, &len); - let len = BigEndian::read_u32(&len) as usize; - - if len > MAXIMUM_PACKET_LEN { - return Err(Error::PacketSize(len)); - } - - buffer.len = len + cipher.tag_len(); - trace!("reading, clear len = {:?}", buffer.len); - } - } - - buffer.buffer.resize(buffer.len + 4, 0); - trace!("read_exact {:?}", buffer.len + 4); - - let l = cipher.packet_length_to_read_for_block_length(); - - #[allow(clippy::indexing_slicing)] // length checked - stream.read_exact(&mut buffer.buffer[l..]).await?; - - trace!("read_exact done"); - let seqn = buffer.seqn.0; - let plaintext = cipher.open(seqn, &mut buffer.buffer)?; - - let padding_length = *plaintext.first().to_owned().unwrap_or(&0) as usize; - trace!("reading, padding_length {padding_length:?}"); - let plaintext_end = plaintext - .len() - .checked_sub(padding_length) - .ok_or(Error::IndexOutOfBounds)?; - - // Sequence numbers are on 32 bits and wrap. - // https://tools.ietf.org/html/rfc4253#section-6.4 - buffer.seqn += Wrapping(1); - buffer.len = 0; - - // Remove the padding - buffer.buffer.resize(plaintext_end + 4, 0); - - Ok(plaintext_end + 4) -} - -pub(crate) const PACKET_LENGTH_LEN: usize = 4; - -const MINIMUM_PACKET_LEN: usize = 16; -// Keep the transport limit aligned with the 256 KiB channel packet baseline. -const MAXIMUM_PACKET_LEN_BASELINE: usize = 256 * 1024; -const CHANNEL_DATA_PACKET_OVERHEAD: usize = 1 + 4 + 4; -const CHANNEL_EXTENDED_DATA_PACKET_OVERHEAD: usize = CHANNEL_DATA_PACKET_OVERHEAD + 4; -const PADDING_LENGTH_LEN: usize = 1; -// SSH requires at least four bytes of padding; with 16-byte blocks, that means -// a full-size channel packet can need up to 19 bytes of transport padding. -const MAXIMUM_PADDING_LEN: usize = 19; -const MAXIMUM_PACKET_LEN_HEADROOM: usize = - PADDING_LENGTH_LEN + CHANNEL_EXTENDED_DATA_PACKET_OVERHEAD + MAXIMUM_PADDING_LEN; -const MAXIMUM_PACKET_LEN: usize = MAXIMUM_PACKET_LEN_BASELINE + MAXIMUM_PACKET_LEN_HEADROOM; -// Keep post-decompression growth within the same packet-acceptance model as -// the transport read path. -pub(crate) const MAXIMUM_DECOMPRESSED_PACKET_LEN: usize = MAXIMUM_PACKET_LEN; - -#[cfg(feature = "_bench")] -pub mod benchmark; diff --git a/crates/bssh-russh/src/client/encrypted.rs b/crates/bssh-russh/src/client/encrypted.rs deleted file mode 100644 index 86ca494c..00000000 --- a/crates/bssh-russh/src/client/encrypted.rs +++ /dev/null @@ -1,1124 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -use std::convert::TryInto; -use std::ops::Deref; -use std::str::FromStr; - -use bytes::Bytes; -use log::{debug, error, info, trace, warn}; -use ssh_encoding::{Decode, Encode, Reader}; -use ssh_key::Algorithm; - -use super::IncomingSshPacket; -use crate::auth::AuthRequest; -use crate::cert::PublicKeyOrCertificate; -use crate::client::{Handler, Msg, Prompt, Reply, Session}; -use crate::helpers::{AlgorithmExt, EncodedExt, NameList, sign_with_hash_alg}; -use crate::keys::key::parse_public_key; -use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage, ensure_end}; -use crate::session::{Encrypted, EncryptedState, GlobalRequestResponse}; -use crate::{ - Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, Error, MethodSet, Sig, auth, - map_err, msg, -}; - -impl Session { - pub(crate) async fn client_read_encrypted( - &mut self, - client: &mut H, - pkt: &mut IncomingSshPacket, - ) -> Result<(), H::Error> { - #[allow(clippy::indexing_slicing)] // length checked - { - trace!( - "client_read_encrypted, buf = {:?}", - &pkt.buffer[..pkt.buffer.len().min(20)] - ); - } - - self.process_packet(client, &pkt.buffer).await - } - - pub(crate) async fn process_packet( - &mut self, - client: &mut H, - buf: &[u8], - ) -> Result<(), H::Error> { - // If we've successfully read a packet. - trace!("process_packet buf = {:?} bytes", buf.len()); - let mut is_authenticated = false; - if let Some(ref mut enc) = self.common.encrypted { - match enc.state { - EncryptedState::WaitingAuthServiceRequest { - ref mut accepted, .. - } => { - debug!( - "waiting service request, {:?} {:?}", - buf.first(), - msg::SERVICE_ACCEPT - ); - match buf.split_first() { - Some((&msg::SERVICE_ACCEPT, mut r)) => { - if map_err!(Bytes::decode(&mut r))?.as_ref() == b"ssh-userauth" { - map_err!(ensure_end(&r))?; - *accepted = true; - if let Some(ref meth) = self.common.auth_method { - let len = enc.write.len(); - let auth_request = AuthRequest::new(meth); - #[allow(clippy::indexing_slicing)] // length checked - if enc.write_auth_request(&self.common.auth_user, meth)? { - debug!("enc: {:?}", &enc.write[len..]); - enc.state = EncryptedState::WaitingAuthRequest(auth_request) - } - } else { - debug!("no auth method") - } - } - } - Some((&msg::EXT_INFO, mut r)) => { - return self.handle_ext_info(&mut r).map_err(Into::into); - } - other => { - debug!("unknown message: {other:?}"); - return Err(crate::Error::Inconsistent.into()); - } - } - } - EncryptedState::WaitingAuthRequest(ref mut auth_request) => { - trace!("waiting auth request, {:?}", buf.first(),); - match buf.split_first() { - Some((&msg::USERAUTH_SUCCESS, r)) => { - map_err!(ensure_end(&r))?; - debug!("userauth_success"); - self.sender - .send(Reply::AuthSuccess) - .map_err(|_| crate::Error::SendError)?; - enc.state = EncryptedState::InitCompression; - if enc.server_compression.is_deferred() { - enc.server_compression.init_decompress(&mut enc.decompress); - } - return Ok(()); - } - Some((&msg::USERAUTH_BANNER, mut r)) => { - let banner = map_err!(String::decode(&mut r))?; - let _language_tag = map_err!(String::decode(&mut r))?; - map_err!(ensure_end(&r))?; - client.auth_banner(&banner, self).await?; - return Ok(()); - } - Some((&msg::USERAUTH_FAILURE, mut r)) => { - debug!("userauth_failure"); - - let remaining_methods: MethodSet = - (&map_err!(NameList::decode(&mut r))?).into(); - let partial_success = map_err!(u8::decode(&mut r))? != 0; - map_err!(ensure_end(&r))?; - debug!( - "remaining methods {remaining_methods:?}, partial success {partial_success:?}" - ); - auth_request.methods = remaining_methods.clone(); - - let no_more_methods = auth_request.methods.is_empty(); - self.common.auth_method = None; - self.sender - .send(Reply::AuthFailure { - proceed_with_methods: remaining_methods, - partial_success, - }) - .map_err(|_| crate::Error::SendError)?; - - // If no other authentication method is allowed by the server, give up. - if no_more_methods { - return Err(crate::Error::NoAuthMethod.into()); - } - } - Some((&msg::USERAUTH_INFO_REQUEST_OR_USERAUTH_PK_OK, mut r)) => { - if let Some(auth::CurrentRequest::PublicKey { - ref mut sent_pk_ok, - .. - }) = auth_request.current - { - debug!("userauth_pk_ok"); - let _algo = map_err!(String::decode(&mut r))?; - let _key = map_err!(Bytes::decode(&mut r))?; - map_err!(ensure_end(&r))?; - *sent_pk_ok = true; - } else if let Some(auth::CurrentRequest::KeyboardInteractive { - .. - }) = auth_request.current - { - debug!("keyboard_interactive"); - - // read fields - let name = map_err!(String::decode(&mut r))?; - - let instructions = map_err!(String::decode(&mut r))?; - - let _lang = map_err!(String::decode(&mut r))?; - let n_prompts = map_err!(u32::decode(&mut r))?; - - // read prompts - // Each prompt needs at least a 4-byte length plus a 1-byte echo flag. - let max_prompts = r.remaining_len() / 5; - let n_prompts = n_prompts as usize; - if n_prompts > max_prompts { - return Err(crate::Error::Inconsistent.into()); - } - let mut prompts = Vec::with_capacity(n_prompts); - for _i in 0..n_prompts { - let prompt = map_err!(String::decode(&mut r))?; - - let echo = map_err!(u8::decode(&mut r))? != 0; - prompts.push(Prompt { - prompt: prompt.to_string(), - echo, - }); - } - map_err!(ensure_end(&r))?; - - // send challenges to caller - self.sender - .send(Reply::AuthInfoRequest { - name, - instructions, - prompts, - }) - .map_err(|_| crate::Error::SendError)?; - - // wait for response from handler - let responses = loop { - match self.receiver.recv().await { - Some(Msg::AuthInfoResponse { responses }) => { - break responses; - } - None => return Err(crate::Error::RecvError.into()), - _ => {} - } - }; - // write responses - enc.client_send_auth_response(&responses)?; - return Ok(()); - } - - // continue with userauth_pk_ok - match self.common.auth_method.take() { - Some(auth_method @ auth::Method::PublicKey { .. }) => { - self.common.buffer.clear(); - enc.client_send_signature( - &self.common.auth_user, - &auth_method, - &mut self.common.buffer, - )? - } - Some(auth_method @ auth::Method::OpenSshCertificate { .. }) => { - self.common.buffer.clear(); - enc.client_send_signature( - &self.common.auth_user, - &auth_method, - &mut self.common.buffer, - )? - } - Some(auth::Method::FuturePublicKey { key, hash_alg }) => { - debug!("public key"); - self.common.buffer.clear(); - let i = enc.client_make_to_sign( - &self.common.auth_user, - &PublicKeyOrCertificate::PublicKey { - key: key.clone(), - hash_alg, - }, - &mut self.common.buffer, - )?; - let len = self.common.buffer.len(); - let buf = std::mem::take(&mut self.common.buffer); - - self.sender - .send(Reply::SignRequest { key, data: buf }) - .map_err(|_| crate::Error::SendError)?; - self.common.buffer = loop { - match self.receiver.recv().await { - Some(Msg::Signed { data }) => break data[..].to_vec(), - None => return Err(crate::Error::RecvError.into()), - _ => {} - } - }; - if self.common.buffer.len() != len { - // The buffer was modified. - push_packet!(enc.write, { - #[allow(clippy::indexing_slicing)] // length checked - enc.write.extend_from_slice(&self.common.buffer[i..]); - }) - } - } - Some(auth::Method::FutureCertificate { cert, hash_alg }) => { - debug!("certificate"); - self.common.buffer.clear(); - let i = enc.client_make_to_sign( - &self.common.auth_user, - &PublicKeyOrCertificate::Certificate(cert.clone()), - &mut self.common.buffer, - )?; - let len = self.common.buffer.len(); - let buf = std::mem::take(&mut self.common.buffer); - - self.sender - .send(Reply::SignRequestCert { - cert, - hash_alg, - data: buf, - }) - .map_err(|_| crate::Error::SendError)?; - self.common.buffer = loop { - match self.receiver.recv().await { - Some(Msg::Signed { data }) => break data, - None => return Err(crate::Error::RecvError.into()), - _ => {} - } - }; - if self.common.buffer.len() != len { - // The buffer was modified. - push_packet!(enc.write, { - #[allow(clippy::indexing_slicing)] // length checked - enc.write.extend(&self.common.buffer[i..]); - }) - } - } - _ => {} - } - } - Some((&msg::EXT_INFO, mut r)) => { - return self.handle_ext_info(&mut r).map_err(Into::into); - } - other => { - debug!("unknown message: {other:?}"); - return Err(crate::Error::Inconsistent.into()); - } - } - } - EncryptedState::InitCompression => unreachable!(), - EncryptedState::Authenticated => is_authenticated = true, - } - } - if is_authenticated { - self.client_read_authenticated(client, buf).await - } else { - Ok(()) - } - } - - fn handle_ext_info(&mut self, r: &mut impl Reader) -> Result<(), Error> { - let n_extensions = u32::decode(r)? as usize; - debug!("Received EXT_INFO, {n_extensions:?} extensions"); - for _ in 0..n_extensions { - let name = String::decode(r)?; - if name == "server-sig-algs" { - self.handle_server_sig_algs_ext(r)?; - } else { - let data = Vec::::decode(r)?; - debug!("* {name:?} (unknown, data: {data:?})"); - } - if let Some(ref mut enc) = self.common.encrypted { - enc.received_extensions.push(name.clone()); - if let Some(mut senders) = enc.extension_info_awaiters.remove(&name) { - senders.drain(..).for_each(|w| { - let _ = w.send(()); - }); - } - } - } - ensure_end(r)?; - Ok(()) - } - - fn handle_server_sig_algs_ext(&mut self, r: &mut impl Reader) -> Result<(), Error> { - let algs = NameList::decode(r)?; - debug!("* server-sig-algs"); - self.server_sig_algs = Some( - algs.iter() - .filter_map(|x| Algorithm::from_str(x).ok()) - .inspect(|x| { - debug!(" * {x:?}"); - }) - .collect::>(), - ); - Ok(()) - } - - async fn client_read_authenticated( - &mut self, - client: &mut H, - buf: &[u8], - ) -> Result<(), H::Error> { - match buf.split_first() { - Some((&msg::CHANNEL_OPEN_CONFIRMATION, mut reader)) => { - debug!("channel_open_confirmation"); - let msg = map_err!(ChannelOpenConfirmation::decode(&mut reader))?; - map_err!(ensure_end(&reader))?; - let local_id = ChannelId(msg.recipient_channel); - - if let Some(ref mut enc) = self.common.encrypted { - if let Some(parameters) = enc.channels.get_mut(&local_id) { - parameters.confirm(&msg); - } else { - // We've not requested this channel, close connection. - return Err(crate::Error::Inconsistent.into()); - } - } else { - return Err(crate::Error::Inconsistent.into()); - }; - - if let Some(channel) = self.channels.get(&local_id) { - channel - .send(ChannelMsg::Open { - id: local_id, - max_packet_size: msg.maximum_packet_size, - window_size: msg.initial_window_size, - }) - .await - .unwrap_or(()); - } else { - error!("no channel for id {local_id:?}"); - } - - client - .channel_open_confirmation( - local_id, - msg.maximum_packet_size, - msg.initial_window_size, - self, - ) - .await - } - Some((&msg::CHANNEL_CLOSE, mut r)) => { - debug!("channel_close"); - let channel_num = map_err!(ChannelId::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(ref mut enc) = self.common.encrypted { - // The CHANNEL_CLOSE message must be sent to the server at this point or the session - // will not be released. - enc.close(channel_num)?; - } - // Forward the close to the channel before removing it, so that - // consumers waiting on `Channel::wait()` receive an explicit - // `ChannelMsg::Close` instead of just seeing `None`. - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::Close).await; - } - self.channels.remove(&channel_num); - client.channel_close(channel_num, self).await - } - Some((&msg::CHANNEL_EOF, mut r)) => { - debug!("channel_eof"); - let channel_num = map_err!(ChannelId::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::Eof).await; - } - client.channel_eof(channel_num, self).await - } - Some((&msg::CHANNEL_OPEN_FAILURE, mut r)) => { - debug!("channel_open_failure"); - let channel_num = map_err!(ChannelId::decode(&mut r))?; - let reason_code = ChannelOpenFailure::from_u32(map_err!(u32::decode(&mut r))?) - .unwrap_or(ChannelOpenFailure::Unknown); - let descr = map_err!(String::decode(&mut r))?; - let language = map_err!(String::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(ref mut enc) = self.common.encrypted { - enc.channels.remove(&channel_num); - } - - if let Some(sender) = self.channels.remove(&channel_num) { - let _ = sender.send(ChannelMsg::OpenFailure(reason_code)).await; - } - - let _ = self.sender.send(Reply::ChannelOpenFailure); - - client - .channel_open_failure(channel_num, reason_code, &descr, &language, self) - .await - } - Some((&msg::CHANNEL_DATA, mut r)) => { - trace!("channel_data"); - let channel_num = map_err!(ChannelId::decode(&mut r))?; - let data = map_err!(Bytes::decode(&mut r))?; - map_err!(ensure_end(&r))?; - let target = self.common.config.window_size; - if let Some(ref mut enc) = self.common.encrypted { - if enc.adjust_window_size(channel_num, &data, target)? { - let next_window = - client.adjust_window(channel_num, self.target_window_size); - if next_window > 0 { - self.target_window_size = next_window - } - } - } - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::Data { data: data.clone() }).await; - } - - client.data(channel_num, &data, self).await - } - Some((&msg::CHANNEL_EXTENDED_DATA, mut r)) => { - debug!("channel_extended_data"); - let channel_num = map_err!(ChannelId::decode(&mut r))?; - let extended_code = map_err!(u32::decode(&mut r))?; - let data = map_err!(Bytes::decode(&mut r))?; - map_err!(ensure_end(&r))?; - let target = self.common.config.window_size; - if let Some(ref mut enc) = self.common.encrypted { - if enc.adjust_window_size(channel_num, &data, target)? { - let next_window = - client.adjust_window(channel_num, self.target_window_size); - if next_window > 0 { - self.target_window_size = next_window - } - } - } - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::ExtendedData { - ext: extended_code, - data: data.clone(), - }) - .await; - } - - client - .extended_data(channel_num, extended_code, &data, self) - .await - } - Some((&msg::CHANNEL_REQUEST, mut r)) => { - let channel_num = map_err!(ChannelId::decode(&mut r))?; - let req = map_err!(String::decode(&mut r))?; - debug!("channel_request: {channel_num:?} {req:?}",); - match req.as_str() { - "xon-xoff" => { - map_err!(u8::decode(&mut r))?; // should be 0. - let client_can_do = map_err!(u8::decode(&mut r))? != 0; - map_err!(ensure_end(&r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::XonXoff { client_can_do }).await; - } - client.xon_xoff(channel_num, client_can_do, self).await - } - "exit-status" => { - map_err!(u8::decode(&mut r))?; // should be 0. - let exit_status = map_err!(u32::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::ExitStatus { exit_status }).await; - } - client.exit_status(channel_num, exit_status, self).await - } - "exit-signal" => { - map_err!(u8::decode(&mut r))?; // should be 0. - let signal_name = - Sig::from_name(map_err!(String::decode(&mut r))?.as_str()); - let core_dumped = map_err!(u8::decode(&mut r))? != 0; - let error_message = map_err!(String::decode(&mut r))?; - let lang_tag = map_err!(String::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::ExitSignal { - signal_name: signal_name.clone(), - core_dumped, - error_message: error_message.to_string(), - lang_tag: lang_tag.to_string(), - }) - .await; - } - client - .exit_signal( - channel_num, - signal_name, - core_dumped, - &error_message, - &lang_tag, - self, - ) - .await - } - "keepalive@openssh.com" => { - let wants_reply = map_err!(u8::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if wants_reply == 1 { - if let Some(ref mut enc) = self.common.encrypted { - trace!("Received channel keep alive message: {req:?}",); - self.common.wants_reply = false; - if let Some(ch) = enc.channels.get(&channel_num) { - push_packet!(enc.write, { - map_err!(msg::CHANNEL_SUCCESS.encode(&mut enc.write))?; - map_err!(ch.recipient_channel.encode(&mut enc.write))?; - }); - } - } - } else { - warn!("Received keepalive without reply request!"); - } - Ok(()) - } - _ => { - let wants_reply = map_err!(u8::decode(&mut r))?; - if wants_reply == 1 { - if let Some(ref mut enc) = self.common.encrypted { - self.common.wants_reply = false; - if let Some(ch) = enc.channels.get(&channel_num) { - push_packet!(enc.write, { - map_err!(msg::CHANNEL_FAILURE.encode(&mut enc.write))?; - map_err!(ch.recipient_channel.encode(&mut enc.write))?; - }) - } - } - } - info!("Unknown channel request {req:?} {wants_reply:?}",); - Ok(()) - } - } - } - Some((&msg::CHANNEL_WINDOW_ADJUST, mut r)) => { - let channel_num = map_err!(ChannelId::decode(&mut r))?; - let amount = map_err!(u32::decode(&mut r))?; - map_err!(ensure_end(&r))?; - let mut new_size = 0; - debug!("channel_window_adjust amount: {amount:?}"); - if let Some(ref mut enc) = self.common.encrypted { - if let Some(ref mut channel) = enc.channels.get_mut(&channel_num) { - new_size = channel.recipient_window_size.saturating_add(amount); - channel.recipient_window_size = new_size; - } else { - return Ok(()); - } - } - - let common = &mut self.common; - if let Some(enc) = common.encrypted.as_mut() { - new_size -= enc - .flush_pending_with_writer(&mut common.packet_writer, channel_num)? - as u32; - } - if let Some(chan) = self.channels.get(&channel_num) { - chan.window_size().update(new_size).await; - // Use try_send to avoid blocking the session loop when channel buffer is full. - // WindowAdjusted is informational - the critical side effect (updating - // WindowSizeRef and notifying ChannelTx) already happens in update(). - let _ = chan.try_send(ChannelMsg::WindowAdjusted { new_size }); - } - client.window_adjusted(channel_num, new_size, self).await - } - Some((&msg::GLOBAL_REQUEST, mut r)) => { - let req = map_err!(String::decode(&mut r))?; - let wants_reply = map_err!(u8::decode(&mut r))?; - if let Some(ref mut enc) = self.common.encrypted { - if req.starts_with("keepalive") { - map_err!(ensure_end(&r))?; - if wants_reply == 1 { - trace!("Received keep alive message: {req:?}",); - self.common.wants_reply = false; - push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)); - } else { - warn!("Received keepalive without reply request!"); - } - } else if req == "hostkeys-00@openssh.com" { - let mut keys = vec![]; - while !r.is_empty() { - let key_blob = map_err!(Bytes::decode(&mut r))?; - match parse_public_key(&key_blob) { - Ok(key) => keys.push(key), - Err(ref err) => { - debug!( - "failed to parse announced host key {key_blob:?}: {err:?}", - ) - } - } - } - map_err!(ensure_end(&r))?; - return client.openssh_ext_host_keys_announced(keys, self).await; - } else { - warn!("Unhandled global request: {req:?} {wants_reply:?}",); - self.common.wants_reply = false; - push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) - } - } - self.common.received_data = false; - Ok(()) - } - Some((&msg::CHANNEL_SUCCESS, mut r)) => { - let channel_num = map_err!(ChannelId::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::Success).await; - } - client.channel_success(channel_num, self).await - } - Some((&msg::CHANNEL_FAILURE, mut r)) => { - let channel_num = map_err!(ChannelId::decode(&mut r))?; - map_err!(ensure_end(&r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan.send(ChannelMsg::Failure).await; - } - client.channel_failure(channel_num, self).await - } - Some((&msg::CHANNEL_OPEN, mut r)) => { - let msg = OpenChannelMessage::parse(&mut r)?; - - if let Some(ref mut enc) = self.common.encrypted { - let id = enc.new_channel_id(); - let channel = ChannelParams { - recipient_channel: msg.recipient_channel, - sender_channel: id, - recipient_window_size: msg.recipient_window_size, - sender_window_size: self.common.config.window_size, - recipient_maximum_packet_size: msg.recipient_maximum_packet_size, - sender_maximum_packet_size: self.common.config.maximum_packet_size, - confirmed: true, - wants_reply: false, - pending_data: std::collections::VecDeque::new(), - pending_eof: false, - pending_close: false, - }; - - let confirm = || { - debug!("confirming channel: {msg:?}"); - map_err!(msg.confirm( - &mut enc.write, - id.0, - channel.sender_window_size, - channel.sender_maximum_packet_size, - ))?; - enc.channels.insert(id, channel); - Ok(()) - }; - - match &msg.typ { - ChannelType::Session => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client.server_channel_open_session(channel, self).await? - } - ChannelType::DirectTcpip(d) => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client - .server_channel_open_direct_tcpip( - channel, - &d.host_to_connect, - d.port_to_connect, - &d.originator_address, - d.originator_port, - self, - ) - .await? - } - ChannelType::DirectStreamLocal(d) => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client - .server_channel_open_direct_streamlocal( - channel, - &d.socket_path, - self, - ) - .await? - } - ChannelType::X11 { - originator_address, - originator_port, - } => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client - .server_channel_open_x11( - channel, - originator_address, - *originator_port, - self, - ) - .await? - } - ChannelType::ForwardedTcpIp(d) => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client - .server_channel_open_forwarded_tcpip( - channel, - &d.host_to_connect, - d.port_to_connect, - &d.originator_address, - d.originator_port, - self, - ) - .await? - } - ChannelType::ForwardedStreamLocal(d) => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client - .server_channel_open_forwarded_streamlocal( - channel, - &d.socket_path, - self, - ) - .await?; - } - ChannelType::AgentForward => { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client - .server_channel_open_agent_forward(channel, self) - .await? - } - ChannelType::Unknown { typ } => { - if client.should_accept_unknown_server_channel(id, typ).await { - confirm()?; - let channel = self.accept_server_initiated_channel(id, &msg); - client.server_channel_open_unknown(channel, self).await?; - } else { - debug!("unknown channel type: {typ}"); - msg.unknown_type(&mut enc.write)?; - } - } - }; - Ok(()) - } else { - Err(crate::Error::Inconsistent.into()) - } - } - Some((&msg::REQUEST_SUCCESS, mut r)) => { - trace!("Global Request Success"); - match self.open_global_requests.pop_front() { - Some(GlobalRequestResponse::Keepalive) => { - map_err!(ensure_end(&r))?; - // ignore keepalives - } - Some(GlobalRequestResponse::Ping(return_channel)) => { - map_err!(ensure_end(&r))?; - let _ = return_channel.send(()); - } - Some(GlobalRequestResponse::NoMoreSessions) => { - map_err!(ensure_end(&r))?; - debug!("no-more-sessions@openssh.com requests success"); - } - Some(GlobalRequestResponse::TcpIpForward(return_channel)) => { - let result = if r.is_empty() { - // If a specific port was requested, the reply has no data - Some(0) - } else { - match u32::decode(&mut r) { - Ok(port) => { - if let Err(e) = ensure_end(&r) { - error!( - "Error parsing port for TcpIpForward request: {e:?}" - ); - None - } else { - Some(port) - } - } - Err(e) => { - error!("Error parsing port for TcpIpForward request: {e:?}"); - None - } - } - }; - let _ = return_channel.send(result); - } - Some(GlobalRequestResponse::CancelTcpIpForward(return_channel)) => { - map_err!(ensure_end(&r))?; - let _ = return_channel.send(true); - } - Some(GlobalRequestResponse::StreamLocalForward(return_channel)) => { - map_err!(ensure_end(&r))?; - let _ = return_channel.send(true); - } - Some(GlobalRequestResponse::CancelStreamLocalForward(return_channel)) => { - map_err!(ensure_end(&r))?; - let _ = return_channel.send(true); - } - None => { - error!("Received global request failure for unknown request!") - } - } - Ok(()) - } - Some((&msg::REQUEST_FAILURE, r)) => { - trace!("global request failure"); - map_err!(ensure_end(&r))?; - match self.open_global_requests.pop_front() { - Some(GlobalRequestResponse::Keepalive) => { - // ignore keepalives - } - Some(GlobalRequestResponse::Ping(return_channel)) => { - let _ = return_channel.send(()); - } - Some(GlobalRequestResponse::NoMoreSessions) => { - warn!("no-more-sessions@openssh.com requests failure"); - } - Some(GlobalRequestResponse::TcpIpForward(return_channel)) => { - let _ = return_channel.send(None); - } - Some(GlobalRequestResponse::CancelTcpIpForward(return_channel)) => { - let _ = return_channel.send(false); - } - Some(GlobalRequestResponse::StreamLocalForward(return_channel)) => { - let _ = return_channel.send(false); - } - Some(GlobalRequestResponse::CancelStreamLocalForward(return_channel)) => { - let _ = return_channel.send(false); - } - None => { - error!("Received global request failure for unknown request!") - } - } - Ok(()) - } - m => { - debug!("unknown message received: {m:?}"); - Ok(()) - } - } - } - - fn accept_server_initiated_channel( - &mut self, - id: ChannelId, - msg: &OpenChannelMessage, - ) -> Channel { - let (channel, channel_ref) = Channel::new( - id, - self.inbound_channel_sender.clone(), - msg.recipient_maximum_packet_size, - msg.recipient_window_size, - self.common.config.channel_buffer_size, - ); - - self.channels.insert(id, channel_ref); - - channel - } - - pub(crate) fn write_auth_request_if_needed( - &mut self, - user: &str, - meth: auth::Method, - ) -> Result { - let mut is_waiting = false; - if let Some(ref mut enc) = self.common.encrypted { - is_waiting = match enc.state { - EncryptedState::WaitingAuthRequest(_) => true, - EncryptedState::WaitingAuthServiceRequest { - accepted, - ref mut sent, - } => { - debug!("sending ssh-userauth service requset"); - if !*sent { - self.common.packet_writer.write_packet(|w| { - msg::SERVICE_REQUEST.encode(w)?; - "ssh-userauth".encode(w)?; - Ok(()) - })?; - *sent = true - } - accepted - } - EncryptedState::InitCompression | EncryptedState::Authenticated => false, - }; - debug!("write_auth_request_if_needed: is_waiting = {is_waiting:?}"); - if is_waiting { - enc.write_auth_request(user, &meth)?; - let auth_request = AuthRequest::new(&meth); - enc.state = EncryptedState::WaitingAuthRequest(auth_request); - } - } - self.common.auth_user.clear(); - self.common.auth_user.push_str(user); - self.common.auth_method = Some(meth); - Ok(is_waiting) - } -} - -impl Encrypted { - fn write_auth_request( - &mut self, - user: &str, - auth_method: &auth::Method, - ) -> Result { - // The server is waiting for our USERAUTH_REQUEST. - Ok(push_packet!(self.write, { - self.write.push(msg::USERAUTH_REQUEST); - - match *auth_method { - auth::Method::None => { - user.encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "none".encode(&mut self.write)?; - true - } - auth::Method::Password { ref password } => { - user.encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "password".encode(&mut self.write)?; - 0u8.encode(&mut self.write)?; - password.encode(&mut self.write)?; - true - } - auth::Method::PublicKey { ref key } => { - user.encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "publickey".encode(&mut self.write)?; - self.write.push(0); // This is a probe - - debug!("write_auth_request: key - {:?}", key.algorithm()); - key.algorithm().as_str().encode(&mut self.write)?; - key.public_key().to_bytes()?.encode(&mut self.write)?; - true - } - auth::Method::OpenSshCertificate { ref cert, .. } => { - user.as_bytes().encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "publickey".encode(&mut self.write)?; - self.write.push(0); // This is a probe - - debug!("write_auth_request: cert - {:?}", cert.algorithm()); - cert.algorithm() - .to_certificate_type() - .encode(&mut self.write)?; - cert.to_bytes()?.as_slice().encode(&mut self.write)?; - true - } - auth::Method::FuturePublicKey { ref key, hash_alg } => { - user.as_bytes().encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "publickey".encode(&mut self.write)?; - self.write.push(0); // This is a probe - - key.algorithm() - .with_hash_alg(hash_alg) - .as_str() - .encode(&mut self.write)?; - - key.to_bytes()?.as_slice().encode(&mut self.write)?; - true - } - auth::Method::FutureCertificate { ref cert, .. } => { - user.as_bytes().encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "publickey".encode(&mut self.write)?; - self.write.push(0); // This is a probe - - cert.algorithm() - .to_certificate_type() - .encode(&mut self.write)?; - cert.to_bytes()?.as_slice().encode(&mut self.write)?; - true - } - auth::Method::KeyboardInteractive { ref submethods } => { - debug!("Keyboard interactive"); - user.as_bytes().encode(&mut self.write)?; - "ssh-connection".encode(&mut self.write)?; - "keyboard-interactive".encode(&mut self.write)?; - "".encode(&mut self.write)?; // lang tag is deprecated. Should be empty - submethods.as_bytes().encode(&mut self.write)?; - true - } - } - })) - } - - fn client_make_to_sign( - &mut self, - user: &str, - key: &PublicKeyOrCertificate, - buffer: &mut Vec, - ) -> Result { - buffer.clear(); - self.session_id.as_ref().encode(buffer)?; - - let i0 = buffer.len(); - buffer.push(msg::USERAUTH_REQUEST); - user.encode(buffer)?; - "ssh-connection".encode(buffer)?; - "publickey".encode(buffer)?; - 1u8.encode(buffer)?; - - match key { - PublicKeyOrCertificate::Certificate(cert) => { - cert.algorithm().to_certificate_type().encode(buffer)?; - cert.to_bytes()?.encode(buffer)?; - } - PublicKeyOrCertificate::PublicKey { key, hash_alg } => { - key.algorithm().with_hash_alg(*hash_alg).encode(buffer)?; - key.to_bytes()?.encode(buffer)?; - } - } - Ok(i0) - } - - fn client_send_signature( - &mut self, - user: &str, - method: &auth::Method, - buffer: &mut Vec, - ) -> Result<(), crate::Error> { - match method { - auth::Method::PublicKey { key } => { - let i0 = - self.client_make_to_sign(user, &PublicKeyOrCertificate::from(key), buffer)?; - - // Extend with self-signature. - sign_with_hash_alg(key, buffer)?.encode(&mut *buffer)?; - - push_packet!(self.write, { - #[allow(clippy::indexing_slicing)] // length checked - self.write.extend_from_slice(&buffer[i0..]); - }) - } - auth::Method::OpenSshCertificate { key, cert } => { - let i0 = self.client_make_to_sign( - user, - &PublicKeyOrCertificate::Certificate(cert.clone()), - buffer, - )?; - - // Extend with self-signature. - signature::Signer::try_sign(key.deref(), buffer)? - .encoded()? - .encode(&mut *buffer)?; - - push_packet!(self.write, { - #[allow(clippy::indexing_slicing)] // length checked - self.write.extend_from_slice(&buffer[i0..]); - }) - } - _ => {} - } - Ok(()) - } - - fn client_send_auth_response(&mut self, responses: &[String]) -> Result<(), crate::Error> { - push_packet!(self.write, { - msg::USERAUTH_INFO_RESPONSE.encode(&mut self.write)?; - (responses.len().try_into().unwrap_or(0) as u32).encode(&mut self.write)?; // number of responses - - for r in responses { - r.encode(&mut self.write)?; // write the reponses - } - }); - Ok(()) - } -} diff --git a/crates/bssh-russh/src/client/kex.rs b/crates/bssh-russh/src/client/kex.rs deleted file mode 100644 index 11217c16..00000000 --- a/crates/bssh-russh/src/client/kex.rs +++ /dev/null @@ -1,400 +0,0 @@ -use core::fmt; -use std::cell::RefCell; -use std::fmt::{Debug, Formatter}; -use std::sync::Arc; - -use bytes::Bytes; -use log::{debug, error, warn}; -use ssh_encoding::{Decode, Encode}; -use ssh_key::{Mpint, PublicKey, Signature}; - -use super::IncomingSshPacket; -use crate::client::{Config, NewKeys}; -use crate::kex::dh::groups::DhGroup; -use crate::kex::{KEXES, KexAlgorithm, KexAlgorithmImplementor, KexCause, KexProgress}; -use crate::keys::key::parse_public_key; -use crate::negotiation::{Names, Select}; -use crate::parsing::ensure_end; -use crate::session::Exchange; -use crate::sshbuffer::PacketWriter; -use crate::{CryptoVec, Error, SshId, msg, negotiation, strict_kex_violation}; - -thread_local! { - static HASH_BUFFER: RefCell = RefCell::new(CryptoVec::new()); -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -enum ClientKexState { - Created, - WaitingForGexReply { - names: Names, - kex: KexAlgorithm, - }, - WaitingForDhReply { - // both KexInit and DH init sent - names: Names, - kex: KexAlgorithm, - }, - WaitingForNewKeys { - server_host_key: PublicKey, - newkeys: NewKeys, - }, -} - -pub(crate) struct ClientKex { - exchange: Exchange, - cause: KexCause, - state: ClientKexState, - config: Arc, -} - -impl Debug for ClientKex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut s = f.debug_struct("ClientKex"); - s.field("cause", &self.cause); - match self.state { - ClientKexState::Created => { - s.field("state", &"created"); - } - ClientKexState::WaitingForGexReply { .. } => { - s.field("state", &"waiting for GEX response"); - } - ClientKexState::WaitingForDhReply { .. } => { - s.field("state", &"waiting for DH response"); - } - ClientKexState::WaitingForNewKeys { .. } => { - s.field("state", &"waiting for NEWKEYS"); - } - } - s.finish() - } -} - -impl ClientKex { - pub fn new( - config: Arc, - client_sshid: &SshId, - server_sshid: &[u8], - cause: KexCause, - ) -> Self { - let exchange = Exchange::new(client_sshid.as_kex_hash_bytes(), server_sshid); - Self { - config, - exchange, - cause, - state: ClientKexState::Created, - } - } - - pub fn kexinit(&mut self, output: &mut PacketWriter) -> Result<(), Error> { - self.exchange.client_kex_init = - negotiation::write_kex(&self.config.preferred, output, None)?; - - Ok(()) - } - - pub fn step( - mut self, - input: Option<&mut IncomingSshPacket>, - output: &mut PacketWriter, - ) -> Result, Error> { - match self.state { - ClientKexState::Created => { - // At this point we expect to read the KEXINIT from the other side - - let Some(input) = input else { - return Err(Error::KexInit); - }; - if input.buffer.first() != Some(&msg::KEXINIT) { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::KexInit); - } - - let names = { - // read algorithms from packet. - self.exchange.server_kex_init = input.buffer.clone().into(); - negotiation::Client::read_kex( - &input.buffer, - &self.config.preferred, - None, - &self.cause, - )? - }; - debug!("negotiated algorithms: {names:?}"); - - // seqno has already been incremented after read() - if names.strict_kex() && !self.cause.is_rekey() && input.seqn.0 != 1 { - return Err(strict_kex_violation( - msg::KEXINIT, - input.seqn.0 as usize - 1, - )); - } - - let mut kex = KEXES.get(&names.kex).ok_or(Error::UnknownAlgo)?.make(); - - if kex.skip_exchange() { - // Non-standard no-kex exchange - let newkeys = compute_keys( - Vec::new(), - kex, - names.clone(), - self.exchange.clone(), - self.cause.session_id(), - )?; - - output.write_packet(|w| { - msg::NEWKEYS.encode(w)?; - Ok(()) - })?; - - return Ok(KexProgress::Done { - newkeys, - server_host_key: None, - }); - } - - if kex.is_dh_gex() { - output.write_packet(|w| { - kex.client_dh_gex_init(&self.config.gex, w)?; - Ok(()) - })?; - - self.state = ClientKexState::WaitingForGexReply { names, kex }; - } else { - output.write_packet(|w| { - kex.client_dh(&mut self.exchange.client_ephemeral, w)?; - Ok(()) - })?; - - self.state = ClientKexState::WaitingForDhReply { names, kex }; - } - - Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn: false, - }) - } - ClientKexState::WaitingForGexReply { names, mut kex } => { - let Some(input) = input else { - return Err(Error::KexInit); - }; - - if input.buffer.first() != Some(&msg::KEX_DH_GEX_GROUP) { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::KexInit); - } - - #[allow(clippy::indexing_slicing)] // length checked - let mut r = &input.buffer[1..]; - - let prime = Mpint::decode(&mut r)?; - let generator = Mpint::decode(&mut r)?; - ensure_end(&r)?; - debug!("received gex group: prime={prime}, generator={generator}"); - - let group = DhGroup { - prime: prime.as_bytes().to_vec().into(), - generator: generator.as_bytes().to_vec().into(), - }; - - if group.bit_size() < self.config.gex.min_group_size - || group.bit_size() > self.config.gex.max_group_size - { - warn!( - "DH prime size ({} bits) not within requested range", - group.bit_size() - ); - return Err(Error::KexInit); - } - - let exchange = &mut self.exchange; - exchange.gex = Some((self.config.gex.clone(), group.clone())); - kex.dh_gex_set_group(group)?; - output.write_packet(|w| { - kex.client_dh(&mut exchange.client_ephemeral, w)?; - Ok(()) - })?; - self.state = ClientKexState::WaitingForDhReply { names, kex }; - - Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn: false, - }) - } - ClientKexState::WaitingForDhReply { mut names, mut kex } => { - // At this point, we've sent ECDH_INTI and - // are waiting for the ECDH_REPLY from the server. - - let Some(input) = input else { - return Err(Error::KexInit); - }; - - if names.ignore_guessed { - // Ignore the next packet if (1) it follows and (2) it's not the correct guess. - debug!("ignoring guessed kex"); - names.ignore_guessed = false; - self.state = ClientKexState::WaitingForDhReply { names, kex }; - return Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn: false, - }); - } - - if input.buffer.first() - != Some(match kex.is_dh_gex() { - true => &msg::KEX_DH_GEX_REPLY, - false => &msg::KEX_ECDH_REPLY, - }) - { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::KexInit); - } - - #[allow(clippy::indexing_slicing)] // length checked - let r = &mut &input.buffer[1..]; - - let server_host_key = Bytes::decode(r)?; // server public key. - let server_host_key = parse_public_key(&server_host_key)?; - debug!( - "received server host key: {:?}", - server_host_key.to_openssh() - ); - - let server_ephemeral = Bytes::decode(r)?; - self.exchange - .server_ephemeral - .extend_from_slice(&server_ephemeral); - kex.compute_shared_secret(&self.exchange.server_ephemeral)?; - - let mut pubkey_vec = Vec::new(); - server_host_key.to_bytes()?.encode(&mut pubkey_vec)?; - - let exchange = &self.exchange; - let hash = HASH_BUFFER.with({ - |buffer| { - let mut buffer = buffer.borrow_mut(); - buffer.clear(); - kex.compute_exchange_hash(&pubkey_vec, exchange, &mut buffer) - } - })?; - - let signature = Bytes::decode(r)?; - let mut signature_reader = &signature[..]; - let signature = Signature::decode(&mut signature_reader)?; - ensure_end(&signature_reader)?; - ensure_end(r)?; - - if let Err(e) = - signature::Verifier::verify(&server_host_key, hash.as_ref(), &signature) - { - debug!("wrong server sig: {e:?}"); - return Err(Error::WrongServerSig); - } - - let newkeys = compute_keys( - hash, - kex, - names.clone(), - self.exchange.clone(), - self.cause.session_id(), - )?; - - output.write_packet(|w| { - msg::NEWKEYS.encode(w)?; - Ok(()) - })?; - - let reset_seqn = newkeys.names.strict_kex() || self.cause.is_strict_rekey(); - - self.state = ClientKexState::WaitingForNewKeys { - server_host_key, - newkeys, - }; - - Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn, - }) - } - ClientKexState::WaitingForNewKeys { - server_host_key, - newkeys, - } => { - // At this point the exchange is complete - // and we're waiting for a KEWKEYS packet - let Some(input) = input else { - return Err(Error::KexInit); - }; - - if input.buffer.first() != Some(&msg::NEWKEYS) { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::Kex); - } - - #[allow(clippy::indexing_slicing, reason = "length checked")] - let r = &input.buffer[1..]; - ensure_end(&r)?; - - Ok(KexProgress::Done { - newkeys, - server_host_key: Some(server_host_key), - }) - } - } - } -} - -fn compute_keys( - hash: Vec, - kex: KexAlgorithm, - names: Names, - exchange: Exchange, - session_id: Option<&CryptoVec>, -) -> Result { - let session_id_ref: &[u8] = match session_id { - Some(sid) => sid, - None => &hash, - }; - // Now computing keys. - let c = kex.compute_keys( - session_id_ref, - &hash, - names.cipher, - names.server_mac, - names.client_mac, - false, - )?; - // The session_id stored in NewKeys is sensitive key material - // (used in key derivation), so keep it as CryptoVec. - // On initial exchange the exchange hash becomes the session_id; - // on rekey we already have it as CryptoVec. - let session_id_cv = match session_id { - Some(s) => s.clone(), - None => { - let mut cv = CryptoVec::new(); - cv.extend(&hash); - cv - } - }; - Ok(NewKeys { - exchange, - names, - kex, - key: 0, - cipher: c, - session_id: session_id_cv, - }) -} diff --git a/crates/bssh-russh/src/client/mod.rs b/crates/bssh-russh/src/client/mod.rs deleted file mode 100644 index 6730858c..00000000 --- a/crates/bssh-russh/src/client/mod.rs +++ /dev/null @@ -1,2428 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -//! # Implementing clients -//! -//! Maybe surprisingly, the data types used by Russh to implement -//! clients are relatively more complicated than for servers. This is -//! mostly related to the fact that clients are generally used both in -//! a synchronous way (in the case of SSH, we can think of sending a -//! shell command), and asynchronously (because the server may send -//! unsollicited messages), and hence need to handle multiple -//! interfaces. -//! -//! The [Session](client::Session) is passed to the [Handler](client::Handler) -//! when the client receives data. -//! -//! Check out the following examples: -//! -//! * [Client that connects to a server, runs a command and prints its output](https://github.com/warp-tech/russh/blob/main/russh/examples/client_exec_simple.rs) -//! * [Client that connects to a server, runs a command in a PTY and provides interactive input/output](https://github.com/warp-tech/russh/blob/main/russh/examples/client_exec_interactive.rs) -//! * [SFTP client (with `russh-sftp`)](https://github.com/warp-tech/russh/blob/main/russh/examples/sftp_client.rs) -//! -//! [Session]: client::Session - -use std::borrow::Cow; -use std::collections::{HashMap, VecDeque}; -use std::convert::TryInto; -use std::num::Wrapping; -use std::pin::Pin; -use std::sync::Arc; -#[cfg(not(target_arch = "wasm32"))] -use std::time::Duration; - -use futures::Future; -use futures::task::{Context, Poll}; -use kex::ClientKex; -use log::{debug, error, trace, warn}; -use russh_util::time::Instant; -use ssh_encoding::Decode; -use ssh_key::{Algorithm, Certificate, HashAlg, PrivateKey, PublicKey}; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf}; -use tokio::pin; -use tokio::sync::mpsc::{ - Receiver, Sender, UnboundedReceiver, UnboundedSender, channel, unbounded_channel, -}; -use tokio::sync::oneshot; - -pub use crate::auth::AuthResult; -use crate::channels::{ - Channel, ChannelMsg, ChannelReadHalf, ChannelRef, ChannelWriteHalf, WindowSizeRef, -}; -use crate::cipher::{self, OpeningKey, clear}; -use crate::kex::{KexAlgorithmImplementor, KexCause, KexProgress, SessionKexState}; -use crate::keys::PrivateKeyWithHashAlg; -use crate::msg::{is_kex_msg, validate_server_msg_strict_kex}; -use crate::session::{CommonSession, EncryptedState, GlobalRequestResponse, NewKeys}; -use crate::ssh_read::SshRead; -use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer, SshId}; -use crate::{ - ChannelId, ChannelOpenFailure, Disconnect, Error, Limits, MethodSet, Sig, auth, map_err, msg, - negotiation, -}; - -mod encrypted; -mod kex; -mod session; - -#[cfg(test)] -mod test; - -/// Actual client session's state. -/// -/// It is in charge of multiplexing and keeping track of various channels -/// that may get opened and closed during the lifetime of an SSH session and -/// allows sending messages to the server. -#[derive(Debug)] -pub struct Session { - kex: SessionKexState, - common: CommonSession>, - receiver: Receiver, - sender: UnboundedSender, - channels: HashMap, - target_window_size: u32, - pending_reads: Vec>, - pending_len: u32, - inbound_channel_sender: Sender, - inbound_channel_receiver: Receiver, - open_global_requests: VecDeque, - server_sig_algs: Option>, -} - -impl Drop for Session { - fn drop(&mut self) { - debug!("drop session") - } -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -enum Reply { - AuthSuccess, - AuthFailure { - proceed_with_methods: MethodSet, - partial_success: bool, - }, - ChannelOpenFailure, - SignRequest { - key: ssh_key::PublicKey, - data: Vec, - }, - SignRequestCert { - cert: Certificate, - hash_alg: Option, - data: Vec, - }, - AuthInfoRequest { - name: String, - instructions: String, - prompts: Vec, - }, -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum Msg { - Authenticate { - user: String, - method: auth::Method, - }, - AuthInfoResponse { - responses: Vec, - }, - Signed { - data: Vec, - }, - ChannelOpenSession { - channel_ref: ChannelRef, - }, - ChannelOpenX11 { - originator_address: String, - originator_port: u32, - channel_ref: ChannelRef, - }, - ChannelOpenDirectTcpIp { - host_to_connect: String, - port_to_connect: u32, - originator_address: String, - originator_port: u32, - channel_ref: ChannelRef, - }, - ChannelOpenDirectStreamLocal { - socket_path: String, - channel_ref: ChannelRef, - }, - TcpIpForward { - /// Provide a channel for the reply result to request a reply from the server - reply_channel: Option>>, - address: String, - port: u32, - }, - CancelTcpIpForward { - /// Provide a channel for the reply result to request a reply from the server - reply_channel: Option>, - address: String, - port: u32, - }, - StreamLocalForward { - /// Provide a channel for the reply result to request a reply from the server - reply_channel: Option>, - socket_path: String, - }, - CancelStreamLocalForward { - /// Provide a channel for the reply result to request a reply from the server - reply_channel: Option>, - socket_path: String, - }, - Close { - id: ChannelId, - }, - Disconnect { - reason: Disconnect, - description: String, - language_tag: String, - }, - Channel(ChannelId, ChannelMsg), - Rekey, - AwaitExtensionInfo { - extension_name: String, - reply_channel: oneshot::Sender<()>, - }, - GetServerSigAlgs { - reply_channel: oneshot::Sender>>, - }, - /// Send a keepalive packet to the remote - Keepalive { - want_reply: bool, - }, - Ping { - reply_channel: oneshot::Sender<()>, - }, - NoMoreSessions { - want_reply: bool, - }, -} - -impl From<(ChannelId, ChannelMsg)> for Msg { - fn from((id, msg): (ChannelId, ChannelMsg)) -> Self { - Msg::Channel(id, msg) - } -} - -#[derive(Debug)] -pub enum KeyboardInteractiveAuthResponse { - Success, - Failure { - /// The server suggests to proceed with these auth methods - remaining_methods: MethodSet, - /// The server says that though auth method has been accepted, - /// further authentication is required - partial_success: bool, - }, - InfoRequest { - name: String, - instructions: String, - prompts: Vec, - }, -} - -#[derive(Debug)] -pub struct Prompt { - pub prompt: String, - pub echo: bool, -} - -#[derive(Debug)] -pub struct RemoteDisconnectInfo { - pub reason_code: crate::Disconnect, - pub message: String, - pub lang_tag: String, -} - -#[derive(Debug)] -pub enum DisconnectReason + Send> { - ReceivedDisconnect(RemoteDisconnectInfo), - Error(E), -} - -/// Handle to a session, used to send messages to a client outside of -/// the request/response cycle. -pub struct Handle { - sender: Sender, - receiver: UnboundedReceiver, - join: russh_util::runtime::JoinHandle>, - channel_buffer_size: usize, -} - -impl Drop for Handle { - fn drop(&mut self) { - debug!("drop handle") - } -} - -impl Handle { - pub fn is_closed(&self) -> bool { - self.sender.is_closed() - } - - /// Perform no authentication. This is useful for testing, but should not be - /// used in most other circumstances. - pub async fn authenticate_none>( - &mut self, - user: U, - ) -> Result { - let user = user.into(); - self.sender - .send(Msg::Authenticate { - user, - method: auth::Method::None, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_recv_reply().await - } - - /// Perform password-based SSH authentication. - pub async fn authenticate_password, P: Into>( - &mut self, - user: U, - password: P, - ) -> Result { - let user = user.into(); - self.sender - .send(Msg::Authenticate { - user, - method: auth::Method::Password { - password: password.into(), - }, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_recv_reply().await - } - - /// Initiate Keyboard-Interactive based SSH authentication. - /// - /// * `submethods` - Hints to the server the preferred methods to be used for authentication - pub async fn authenticate_keyboard_interactive_start< - U: Into, - S: Into>, - >( - &mut self, - user: U, - submethods: S, - ) -> Result { - self.sender - .send(Msg::Authenticate { - user: user.into(), - method: auth::Method::KeyboardInteractive { - submethods: submethods.into().unwrap_or_else(|| "".to_owned()), - }, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_recv_keyboard_interactive_reply().await - } - - /// Respond to AuthInfoRequests from the server. A server can send any number of these Requests - /// including empty requests. You may have to call this function multple times in order to - /// complete Keyboard-Interactive based SSH authentication. - /// - /// * `responses` - The responses to each prompt. The number of responses must match the number - /// of prompts. If a prompt has an empty string, then the response should be an empty string. - pub async fn authenticate_keyboard_interactive_respond( - &mut self, - responses: Vec, - ) -> Result { - self.sender - .send(Msg::AuthInfoResponse { responses }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_recv_keyboard_interactive_reply().await - } - - async fn wait_recv_keyboard_interactive_reply( - &mut self, - ) -> Result { - loop { - match self.receiver.recv().await { - Some(Reply::AuthSuccess) => return Ok(KeyboardInteractiveAuthResponse::Success), - Some(Reply::AuthFailure { - proceed_with_methods: remaining_methods, - partial_success, - }) => { - return Ok(KeyboardInteractiveAuthResponse::Failure { - remaining_methods, - partial_success, - }); - } - Some(Reply::AuthInfoRequest { - name, - instructions, - prompts, - }) => { - return Ok(KeyboardInteractiveAuthResponse::InfoRequest { - name, - instructions, - prompts, - }); - } - None => return Err(crate::Error::RecvError), - _ => {} - } - } - } - - async fn wait_recv_reply(&mut self) -> Result { - loop { - match self.receiver.recv().await { - Some(Reply::AuthSuccess) => return Ok(AuthResult::Success), - Some(Reply::AuthFailure { - proceed_with_methods: remaining_methods, - partial_success, - }) => { - return Ok(AuthResult::Failure { - remaining_methods, - partial_success, - }); - } - None => { - return Ok(AuthResult::Failure { - remaining_methods: MethodSet::empty(), - partial_success: false, - }); - } - _ => {} - } - } - } - - /// Perform public key-based SSH authentication. - /// - /// For RSA keys, you'll need to decide on which hash algorithm to use. - /// This is the difference between what is also known as - /// `ssh-rsa`, `rsa-sha2-256`, and `rsa-sha2-512` "keys" in OpenSSH. - /// You can use [Handle::best_supported_rsa_hash] to automatically - /// figure out the best hash algorithm for RSA keys. - pub async fn authenticate_publickey>( - &mut self, - user: U, - key: PrivateKeyWithHashAlg, - ) -> Result { - let user = user.into(); - self.sender - .send(Msg::Authenticate { - user, - method: auth::Method::PublicKey { key }, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_recv_reply().await - } - - /// Perform public OpenSSH Certificate-based SSH authentication - pub async fn authenticate_openssh_cert>( - &mut self, - user: U, - key: Arc, - cert: Certificate, - ) -> Result { - let user = user.into(); - self.sender - .send(Msg::Authenticate { - user, - method: auth::Method::OpenSshCertificate { key, cert }, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_recv_reply().await - } - - /// Authenticate using a custom method that implements the - /// [`Signer`][auth::Signer] trait. Currently, this crate only provides an - /// implementation for an [SSH agent][crate::keys::agent::client::AgentClient]. - pub async fn authenticate_publickey_with, S: auth::Signer>( - &mut self, - user: U, - key: ssh_key::PublicKey, - hash_alg: Option, - signer: &mut S, - ) -> Result { - let user = user.into(); - if self - .sender - .send(Msg::Authenticate { - user, - method: auth::Method::FuturePublicKey { key, hash_alg }, - }) - .await - .is_err() - { - return Err((crate::SendError {}).into()); - } - loop { - let reply = self.receiver.recv().await; - match reply { - Some(Reply::AuthSuccess) => return Ok(AuthResult::Success), - Some(Reply::AuthFailure { - proceed_with_methods: remaining_methods, - partial_success, - }) => { - return Ok(AuthResult::Failure { - remaining_methods, - partial_success, - }); - } - Some(Reply::SignRequest { key, data }) => { - let data = signer.auth_sign(&key.into(), hash_alg, data).await; - let data = match data { - Ok(data) => data, - Err(e) => return Err(e), - }; - if self.sender.send(Msg::Signed { data }).await.is_err() { - return Err((crate::SendError {}).into()); - } - } - None => { - return Ok(AuthResult::Failure { - remaining_methods: MethodSet::empty(), - partial_success: false, - }); - } - _ => {} - } - } - } - - /// Authenticate using a certificate with a custom signer that implements the - /// [`Signer`][auth::Signer] trait. This is for certificate-based authentication - /// where the signing is delegated to an external signer (e.g., SSH agent). - /// - /// For RSA certificates, you can specify the hash algorithm to use. - pub async fn authenticate_certificate_with, S: auth::Signer>( - &mut self, - user: U, - cert: Certificate, - hash_alg: Option, - signer: &mut S, - ) -> Result { - let user = user.into(); - if self - .sender - .send(Msg::Authenticate { - user, - method: auth::Method::FutureCertificate { cert, hash_alg }, - }) - .await - .is_err() - { - return Err((crate::SendError {}).into()); - } - loop { - let reply = self.receiver.recv().await; - match reply { - Some(Reply::AuthSuccess) => return Ok(AuthResult::Success), - Some(Reply::AuthFailure { - proceed_with_methods: remaining_methods, - partial_success, - }) => { - return Ok(AuthResult::Failure { - remaining_methods, - partial_success, - }); - } - Some(Reply::SignRequestCert { - cert, - hash_alg, - data, - }) => { - let data = signer.auth_sign(&cert.into(), hash_alg, data).await; - let data = match data { - Ok(data) => data, - Err(e) => return Err(e), - }; - if self.sender.send(Msg::Signed { data }).await.is_err() { - return Err((crate::SendError {}).into()); - } - } - None => { - return Ok(AuthResult::Failure { - remaining_methods: MethodSet::empty(), - partial_success: false, - }); - } - _ => {} - } - } - } - - /// Wait for confirmation that a channel is open - async fn wait_channel_confirmation( - &self, - mut receiver: Receiver, - window_size_ref: WindowSizeRef, - ) -> Result, crate::Error> { - loop { - match receiver.recv().await { - Some(ChannelMsg::Open { - id, - max_packet_size, - window_size, - }) => { - window_size_ref.update(window_size).await; - - return Ok(Channel { - write_half: ChannelWriteHalf { - id, - sender: self.sender.clone(), - max_packet_size, - window_size: window_size_ref, - }, - read_half: ChannelReadHalf { receiver }, - }); - } - Some(ChannelMsg::OpenFailure(reason)) => { - return Err(crate::Error::ChannelOpenFailure(reason)); - } - None => { - debug!("channel confirmation sender was dropped"); - return Err(crate::Error::Disconnect); - } - msg => { - debug!("msg = {msg:?}"); - } - } - } - } - - /// See [`Handle::best_supported_rsa_hash`]. - #[cfg(not(target_arch = "wasm32"))] - async fn await_extension_info(&self, extension_name: String) -> Result<(), crate::Error> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Msg::AwaitExtensionInfo { - extension_name, - reply_channel: sender, - }) - .await - .map_err(|_| crate::Error::SendError)?; - let _ = tokio::time::timeout(Duration::from_secs(1), receiver).await; - Ok(()) - } - - /// Returns the best RSA hash algorithm supported by the server, - /// as indicated by the `server-sig-algs` extension. - /// If the server does not support the extension, - /// `None` is returned. In this case you may still attempt an authentication - /// with `rsa-sha2-256` or `rsa-sha2-512` and hope for the best. - /// If the server supports the extension, but does not support `rsa-sha2-*`, - /// `Some(None)` is returned. - /// - /// Note that this method will wait for up to 1 second for the server to - /// send the extension info if it hasn't done so yet (except when running under - /// WebAssembly). Unfortunately the timing of the EXT_INFO message cannot be known - /// in advance (RFC 8308). - /// - /// If this method returns `None` once, then for most SSH servers - /// you can assume that it will return `None` every time. - pub async fn best_supported_rsa_hash(&self) -> Result>, Error> { - // Wait for the extension info from the server - #[cfg(not(target_arch = "wasm32"))] - self.await_extension_info("server-sig-algs".into()).await?; - - let (sender, receiver) = oneshot::channel(); - - self.sender - .send(Msg::GetServerSigAlgs { - reply_channel: sender, - }) - .await - .map_err(|_| crate::Error::SendError)?; - - if let Some(ssa) = receiver.await.map_err(|_| Error::Inconsistent)? { - let possible_algs = [ - Some(ssh_key::HashAlg::Sha512), - Some(ssh_key::HashAlg::Sha256), - None, - ]; - for alg in possible_algs.into_iter() { - if ssa.contains(&Algorithm::Rsa { hash: alg }) { - return Ok(Some(alg)); - } - } - } - - Ok(None) - } - - /// Request a session channel (the most basic type of - /// channel). This function returns `Some(..)` immediately if the - /// connection is authenticated, but the channel only becomes - /// usable when it's confirmed by the server, as indicated by the - /// `confirmed` field of the corresponding `Channel`. - pub async fn channel_open_session(&self) -> Result, crate::Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenSession { channel_ref }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - /// Request an X11 channel, on which the X11 protocol may be tunneled. - pub async fn channel_open_x11>( - &self, - originator_address: A, - originator_port: u32, - ) -> Result, crate::Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenX11 { - originator_address: originator_address.into(), - originator_port, - channel_ref, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - /// Open a TCP/IP forwarding channel. This is usually done when a - /// connection comes to a locally forwarded TCP/IP port. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). The - /// TCP/IP packets can then be tunneled through the channel using - /// `.data()`. After writing a stream to a channel using - /// [`.data()`][Channel::data], be sure to call [`.eof()`][Channel::eof] to - /// indicate that no more data will be sent, or you may see hangs when - /// writing large streams. - pub async fn channel_open_direct_tcpip, B: Into>( - &self, - host_to_connect: A, - port_to_connect: u32, - originator_address: B, - originator_port: u32, - ) -> Result, crate::Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenDirectTcpIp { - host_to_connect: host_to_connect.into(), - port_to_connect, - originator_address: originator_address.into(), - originator_port, - channel_ref, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - pub async fn channel_open_direct_streamlocal>( - &self, - socket_path: S, - ) -> Result, crate::Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenDirectStreamLocal { - socket_path: socket_path.into(), - channel_ref, - }) - .await - .map_err(|_| crate::Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - /// Requests the server to open a TCP/IP forward channel - /// - /// If port == 0 the server will choose a port that will be returned, returns 0 otherwise - pub async fn tcpip_forward>( - &self, - address: A, - port: u32, - ) -> Result { - let (reply_send, reply_recv) = oneshot::channel(); - self.sender - .send(Msg::TcpIpForward { - reply_channel: Some(reply_send), - address: address.into(), - port, - }) - .await - .map_err(|_| crate::Error::SendError)?; - - match reply_recv.await { - Ok(Some(port)) => Ok(port), - Ok(None) => Err(crate::Error::RequestDenied), - Err(e) => { - error!("Unable to receive TcpIpForward result: {e:?}"); - Err(crate::Error::Disconnect) - } - } - } - - // Requests the server to close a TCP/IP forward channel - pub async fn cancel_tcpip_forward>( - &self, - address: A, - port: u32, - ) -> Result<(), crate::Error> { - let (reply_send, reply_recv) = oneshot::channel(); - self.sender - .send(Msg::CancelTcpIpForward { - reply_channel: Some(reply_send), - address: address.into(), - port, - }) - .await - .map_err(|_| crate::Error::SendError)?; - - match reply_recv.await { - Ok(true) => Ok(()), - Ok(false) => Err(crate::Error::RequestDenied), - Err(e) => { - error!("Unable to receive CancelTcpIpForward result: {e:?}"); - Err(crate::Error::Disconnect) - } - } - } - - // Requests the server to open a UDS forward channel - pub async fn streamlocal_forward>( - &self, - socket_path: A, - ) -> Result<(), crate::Error> { - let (reply_send, reply_recv) = oneshot::channel(); - self.sender - .send(Msg::StreamLocalForward { - reply_channel: Some(reply_send), - socket_path: socket_path.into(), - }) - .await - .map_err(|_| crate::Error::SendError)?; - - match reply_recv.await { - Ok(true) => Ok(()), - Ok(false) => Err(crate::Error::RequestDenied), - Err(e) => { - error!("Unable to receive StreamLocalForward result: {e:?}"); - Err(crate::Error::Disconnect) - } - } - } - - // Requests the server to close a UDS forward channel - pub async fn cancel_streamlocal_forward>( - &self, - socket_path: A, - ) -> Result<(), crate::Error> { - let (reply_send, reply_recv) = oneshot::channel(); - self.sender - .send(Msg::CancelStreamLocalForward { - reply_channel: Some(reply_send), - socket_path: socket_path.into(), - }) - .await - .map_err(|_| crate::Error::SendError)?; - - match reply_recv.await { - Ok(true) => Ok(()), - Ok(false) => Err(crate::Error::RequestDenied), - Err(e) => { - error!("Unable to receive CancelStreamLocalForward result: {e:?}"); - Err(crate::Error::Disconnect) - } - } - } - - /// Sends a disconnect message. - pub async fn disconnect( - &self, - reason: Disconnect, - description: &str, - language_tag: &str, - ) -> Result<(), crate::Error> { - self.sender - .send(Msg::Disconnect { - reason, - description: description.into(), - language_tag: language_tag.into(), - }) - .await - .map_err(|_| crate::Error::SendError)?; - Ok(()) - } - - /// Send data to the session referenced by this handler. - /// - /// This is useful for server-initiated channels; for channels created by - /// the client, prefer to use the Channel returned from the `open_*` methods. - pub async fn data( - &self, - id: ChannelId, - data: impl Into, - ) -> Result<(), bytes::Bytes> { - let data = data.into(); - self.sender - .send(Msg::Channel(id, ChannelMsg::Data { data })) - .await - .map_err(|e| match e.0 { - Msg::Channel(_, ChannelMsg::Data { data, .. }) => data, - _ => unreachable!(), - }) - } - - /// Asynchronously perform a session re-key at the next opportunity - pub async fn rekey_soon(&self) -> Result<(), Error> { - self.sender - .send(Msg::Rekey) - .await - .map_err(|_| Error::SendError)?; - - Ok(()) - } - - /// Send a keepalive package to the remote peer. - pub async fn send_keepalive(&self, want_reply: bool) -> Result<(), Error> { - self.sender - .send(Msg::Keepalive { want_reply }) - .await - .map_err(|_| Error::SendError) - } - - /// Send a keepalive/ping package to the remote peer, and wait for the reply/pong. - pub async fn send_ping(&self) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Msg::Ping { - reply_channel: sender, - }) - .await - .map_err(|_| Error::SendError)?; - let _ = receiver.await; - Ok(()) - } - - /// Send a no-more-sessions request to the remote peer. - pub async fn no_more_sessions(&self, want_reply: bool) -> Result<(), Error> { - self.sender - .send(Msg::NoMoreSessions { want_reply }) - .await - .map_err(|_| Error::SendError) - } -} - -impl Future for Handle { - type Output = Result<(), H::Error>; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - match Future::poll(Pin::new(&mut self.join), cx) { - Poll::Ready(r) => Poll::Ready(match r { - Ok(Ok(x)) => Ok(x), - Err(e) => Err(crate::Error::from(e).into()), - Ok(Err(e)) => Err(e), - }), - Poll::Pending => Poll::Pending, - } - } -} - -/// Connect to a server at the address specified, using the [`Handler`] -/// (implemented by you) and [`Config`] specified. Returns a future that -/// resolves to a [`Handle`]. This handle can then be used to create channels, -/// which in turn can be used to tunnel TCP connections, request a PTY, execute -/// commands, etc. The future will resolve to an error if the connection fails. -/// This function creates a connection to the `addr` specified using a -/// [`tokio::net::TcpStream`] and then calls [`connect_stream`] under the hood. -#[cfg(not(target_arch = "wasm32"))] -pub async fn connect( - config: Arc, - addrs: A, - handler: H, -) -> Result, H::Error> { - let socket = map_err!(tokio::net::TcpStream::connect(addrs).await)?; - if config.as_ref().nodelay { - if let Err(e) = socket.set_nodelay(true) { - warn!("set_nodelay() failed: {e:?}"); - } - } - - connect_stream(config, socket, handler).await -} - -/// Connect a stream to a server. This stream must implement -/// [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`], as well as [`Unpin`] -/// and [`Send`]. Typically, you may prefer to use [`connect`], which uses a -/// [`tokio::net::TcpStream`] and then calls this function under the hood. -pub async fn connect_stream( - config: Arc, - mut stream: R, - handler: H, -) -> Result, H::Error> -where - H: Handler + Send + 'static, - R: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - // Writing SSH id. - let mut write_buffer = SSHBuffer::new(); - - debug!("ssh id = {:?}", config.as_ref().client_id); - - write_buffer.send_ssh_id(&config.as_ref().client_id); - map_err!(stream.write_all(&write_buffer.buffer).await)?; - - // Reading SSH id and allocating a session if correct. - let mut stream = SshRead::new(stream); - let sshid = stream.read_ssh_id().await?; - - let (handle_sender, session_receiver) = channel(10); - let (session_sender, handle_receiver) = unbounded_channel(); - if config.maximum_packet_size > 65535 { - error!( - "Maximum packet size ({:?}) should not larger than a TCP packet (65535)", - config.maximum_packet_size - ); - } - let channel_buffer_size = config.channel_buffer_size; - let mut session = Session::new( - config.window_size, - CommonSession { - packet_writer: PacketWriter::clear(), - auth_user: String::new(), - auth_attempts: 0, - auth_method: None, // Client only. - remote_to_local: Box::new(clear::Key), - encrypted: None, - config, - wants_reply: false, - disconnected: false, - buffer: Vec::new(), - strict_kex: false, - alive_timeouts: 0, - received_data: false, - remote_sshid: sshid.into(), - }, - session_receiver, - session_sender, - ); - session.begin_rekey()?; - let (kex_done_signal, kex_done_signal_rx) = oneshot::channel(); - let join = russh_util::runtime::spawn(session.run(stream, handler, Some(kex_done_signal))); - - if let Err(err) = kex_done_signal_rx.await { - // kex_done_signal Sender is dropped when the session - // fails before a succesful key exchange - debug!("kex_done_signal sender was dropped {err:?}"); - join.await.map_err(crate::Error::Join)??; - return Err(H::Error::from(crate::Error::Disconnect)); - } - - Ok(Handle { - sender: handle_sender, - receiver: handle_receiver, - join, - channel_buffer_size, - }) -} - -async fn start_reading( - mut stream_read: R, - mut buffer: SSHBuffer, - mut cipher: Box, -) -> Result<(usize, R, SSHBuffer, Box), crate::Error> { - buffer.buffer.clear(); - let n = cipher::read(&mut stream_read, &mut buffer, &mut *cipher).await?; - Ok((n, stream_read, buffer, cipher)) -} - -impl Session { - fn maybe_decompress(&mut self, buffer: &SSHBuffer) -> Result { - if let Some(ref mut enc) = self.common.encrypted { - let mut decomp = Vec::new(); - Ok(IncomingSshPacket { - #[allow(clippy::indexing_slicing)] // length checked - buffer: enc.decompress.decompress( - &buffer.buffer[5..], - &mut decomp, - )?.into(), - seqn: buffer.seqn, - }) - } else { - Ok(IncomingSshPacket { - #[allow(clippy::indexing_slicing)] // length checked - buffer: buffer.buffer[5..].into(), - seqn: buffer.seqn, - }) - } - } - - fn new( - target_window_size: u32, - common: CommonSession>, - receiver: Receiver, - sender: UnboundedSender, - ) -> Self { - let (inbound_channel_sender, inbound_channel_receiver) = channel(10); - Self { - common, - receiver, - sender, - kex: SessionKexState::Idle, - target_window_size, - inbound_channel_sender, - inbound_channel_receiver, - channels: HashMap::new(), - pending_reads: Vec::new(), - pending_len: 0, - open_global_requests: VecDeque::new(), - server_sig_algs: None, - } - } - - async fn run( - mut self, - stream: SshRead, - mut handler: H, - mut kex_done_signal: Option>, - ) -> Result<(), H::Error> { - let (stream_read, mut stream_write) = stream.split(); - let result = self - .run_inner( - stream_read, - &mut stream_write, - &mut handler, - &mut kex_done_signal, - ) - .await; - trace!("disconnected"); - self.receiver.close(); - self.inbound_channel_receiver.close(); - map_err!(stream_write.shutdown().await)?; - match result { - Ok(v) => { - handler - .disconnected(DisconnectReason::ReceivedDisconnect(v)) - .await?; - Ok(()) - } - Err(e) => { - if kex_done_signal.is_some() { - // The kex signal has not been consumed yet, - // so we can send return the concrete error to be propagated - // into the JoinHandle and returned from `connect_stream` - debug!("disconnected during handshake {e:?}"); - Err(e) - } else { - // The kex signal has been consumed, so no one is - // awaiting the result of this coroutine - // We're better off passing the error into the Handler - debug!("disconnected {e:?}"); - handler.disconnected(DisconnectReason::Error(e)).await?; - Err(H::Error::from(crate::Error::Disconnect)) - } - } - } - } - - async fn run_inner( - &mut self, - stream_read: SshRead>, - stream_write: &mut WriteHalf, - handler: &mut H, - kex_done_signal: &mut Option>, - ) -> Result { - let mut result: Result = Err(Error::Disconnect.into()); - self.flush()?; - - map_err!(self.common.packet_writer.flush_into(stream_write).await)?; - - let buffer = SSHBuffer::new(); - - // Allow handing out references to the cipher - let mut opening_cipher = Box::new(clear::Key) as Box; - std::mem::swap(&mut opening_cipher, &mut self.common.remote_to_local); - - let keepalive_timer = - crate::future_or_pending(self.common.config.keepalive_interval, tokio::time::sleep); - pin!(keepalive_timer); - - let inactivity_timer = - crate::future_or_pending(self.common.config.inactivity_timeout, tokio::time::sleep); - pin!(inactivity_timer); - - let reading = start_reading(stream_read, buffer, opening_cipher); - pin!(reading); - - #[allow(clippy::panic)] // false positive in select! macro - while !self.common.disconnected { - self.common.received_data = false; - let mut sent_keepalive = false; - tokio::select! { - r = &mut reading => { - let (stream_read, mut buffer, mut opening_cipher) = match r { - Ok((_, stream_read, buffer, opening_cipher)) => (stream_read, buffer, opening_cipher), - Err(e) => return Err(e.into()) - }; - - std::mem::swap(&mut opening_cipher, &mut self.common.remote_to_local); - - if buffer.buffer.len() < 5 { - break - } - - let mut pkt = self.maybe_decompress(&buffer)?; - if !pkt.buffer.is_empty() { - #[allow(clippy::indexing_slicing)] // length checked - if pkt.buffer[0] == crate::msg::DISCONNECT { - debug!("received disconnect"); - result = self.process_disconnect(&pkt).map_err(H::Error::from); - } else { - self.common.received_data = true; - reply(self, handler, kex_done_signal, &mut pkt).await?; - buffer.seqn = pkt.seqn; // TODO reply changes seqn internall, find cleaner way - } - } - - std::mem::swap(&mut opening_cipher, &mut self.common.remote_to_local); - reading.set(start_reading(stream_read, buffer, opening_cipher)); - } - () = &mut keepalive_timer => { - if let Some(ref mut enc) = self.common.encrypted { - if matches!(enc.state, EncryptedState::Authenticated) { - self.common.alive_timeouts = self.common.alive_timeouts.saturating_add(1); - if self.common.config.keepalive_max != 0 && self.common.alive_timeouts > self.common.config.keepalive_max { - debug!("Timeout, server not responding to keepalives"); - return Err(crate::Error::KeepaliveTimeout.into()); - } - sent_keepalive = true; - self.send_keepalive(true)?; - } - } - } - () = &mut inactivity_timer => { - debug!("timeout"); - return Err(crate::Error::InactivityTimeout.into()); - } - msg = self.receiver.recv(), if !self.kex.active() => { - match msg { - Some(msg) => self.handle_msg(msg)?, - None => { - self.common.disconnected = true; - break - } - }; - - // eagerly take all outgoing messages so writes are batched - while !self.kex.active() { - match self.receiver.try_recv() { - Ok(next) => self.handle_msg(next)?, - Err(_) => break - } - } - } - msg = self.inbound_channel_receiver.recv(), if !self.kex.active() => { - match msg { - Some(msg) => self.handle_msg(msg)?, - None => (), - } - - // eagerly take all outgoing messages so writes are batched - while !self.kex.active() { - match self.inbound_channel_receiver.try_recv() { - Ok(next) => self.handle_msg(next)?, - Err(_) => break - } - } - } - }; - - self.flush()?; - map_err!(self.common.packet_writer.flush_into(stream_write).await)?; - - if let Some(ref mut enc) = self.common.encrypted { - if let EncryptedState::InitCompression = enc.state { - if enc.client_compression.is_deferred() { - enc.client_compression - .init_compress(self.common.packet_writer.compress()); - } - enc.state = EncryptedState::Authenticated; - } - } - - if self.common.received_data { - // Reset the number of failed keepalive attempts. We don't - // bother detecting keepalive response messages specifically - // (OpenSSH_9.6p1 responds with REQUEST_FAILURE aka 82). Instead - // we assume that the server is still alive if we receive any - // data from it. - self.common.alive_timeouts = 0; - } - if self.common.received_data || sent_keepalive { - if let (futures::future::Either::Right(ref mut sleep), Some(d)) = ( - keepalive_timer.as_mut().as_pin_mut(), - self.common.config.keepalive_interval, - ) { - sleep.as_mut().reset(tokio::time::Instant::now() + d); - } - } - if !sent_keepalive { - if let (futures::future::Either::Right(ref mut sleep), Some(d)) = ( - inactivity_timer.as_mut().as_pin_mut(), - self.common.config.inactivity_timeout, - ) { - sleep.as_mut().reset(tokio::time::Instant::now() + d); - } - } - } - - result - } - - fn process_disconnect( - &mut self, - pkt: &IncomingSshPacket, - ) -> Result { - let mut r = &pkt.buffer[..]; - u8::decode(&mut r)?; // skip message type - self.common.disconnected = true; - - let reason_code = u32::decode(&mut r)?.try_into()?; - let message = String::decode(&mut r)?; - let lang_tag = String::decode(&mut r)?; - - Ok(RemoteDisconnectInfo { - reason_code, - message, - lang_tag, - }) - } - - fn handle_msg(&mut self, msg: Msg) -> Result<(), crate::Error> { - match msg { - Msg::Authenticate { user, method } => { - self.write_auth_request_if_needed(&user, method)?; - } - Msg::Signed { .. } => {} - Msg::AuthInfoResponse { .. } => {} - Msg::ChannelOpenSession { channel_ref } => { - let id = self.channel_open_session()?; - self.channels.insert(id, channel_ref); - } - Msg::ChannelOpenX11 { - originator_address, - originator_port, - channel_ref, - } => { - let id = self.channel_open_x11(&originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - } - Msg::ChannelOpenDirectTcpIp { - host_to_connect, - port_to_connect, - originator_address, - originator_port, - channel_ref, - } => { - let id = self.channel_open_direct_tcpip( - &host_to_connect, - port_to_connect, - &originator_address, - originator_port, - )?; - self.channels.insert(id, channel_ref); - } - Msg::ChannelOpenDirectStreamLocal { - socket_path, - channel_ref, - } => { - let id = self.channel_open_direct_streamlocal(&socket_path)?; - self.channels.insert(id, channel_ref); - } - Msg::TcpIpForward { - reply_channel, - address, - port, - } => self.tcpip_forward(reply_channel, &address, port)?, - Msg::CancelTcpIpForward { - reply_channel, - address, - port, - } => self.cancel_tcpip_forward(reply_channel, &address, port)?, - Msg::StreamLocalForward { - reply_channel, - socket_path, - } => self.streamlocal_forward(reply_channel, &socket_path)?, - Msg::CancelStreamLocalForward { - reply_channel, - socket_path, - } => self.cancel_streamlocal_forward(reply_channel, &socket_path)?, - Msg::Disconnect { - reason, - description, - language_tag, - } => self.disconnect(reason, &description, &language_tag)?, - Msg::Channel(id, ChannelMsg::Data { data }) => self.data(id, data)?, - Msg::Channel(id, ChannelMsg::Eof) => { - self.eof(id)?; - } - Msg::Channel(id, ChannelMsg::ExtendedData { data, ext }) => { - self.extended_data(id, ext, data)?; - } - Msg::Channel( - id, - ChannelMsg::RequestPty { - want_reply, - term, - col_width, - row_height, - pix_width, - pix_height, - terminal_modes, - }, - ) => self.request_pty( - id, - want_reply, - &term, - col_width, - row_height, - pix_width, - pix_height, - &terminal_modes, - )?, - Msg::Channel( - id, - ChannelMsg::WindowChange { - col_width, - row_height, - pix_width, - pix_height, - }, - ) => self.window_change(id, col_width, row_height, pix_width, pix_height)?, - Msg::Channel( - id, - ChannelMsg::RequestX11 { - want_reply, - single_connection, - x11_authentication_protocol, - x11_authentication_cookie, - x11_screen_number, - }, - ) => self.request_x11( - id, - want_reply, - single_connection, - &x11_authentication_protocol, - &x11_authentication_cookie, - x11_screen_number, - )?, - Msg::Channel( - id, - ChannelMsg::SetEnv { - want_reply, - variable_name, - variable_value, - }, - ) => self.set_env(id, want_reply, &variable_name, &variable_value)?, - Msg::Channel(id, ChannelMsg::RequestShell { want_reply }) => { - self.request_shell(want_reply, id)? - } - Msg::Channel( - id, - ChannelMsg::Exec { - want_reply, - command, - }, - ) => self.exec(id, want_reply, &command)?, - Msg::Channel(id, ChannelMsg::Signal { signal }) => self.signal(id, signal)?, - Msg::Channel(id, ChannelMsg::RequestSubsystem { want_reply, name }) => { - self.request_subsystem(want_reply, id, &name)? - } - Msg::Channel(id, ChannelMsg::AgentForward { want_reply }) => { - self.agent_forward(id, want_reply)? - } - Msg::Channel(id, ChannelMsg::Close) => self.close(id)?, - Msg::Rekey => self.initiate_rekey()?, - Msg::AwaitExtensionInfo { - extension_name, - reply_channel, - } => { - if let Some(ref mut enc) = self.common.encrypted { - // Drop if the extension has been seen already - if !enc.received_extensions.contains(&extension_name) { - // There will be no new extension info after authentication - // has succeeded - if !matches!(enc.state, EncryptedState::Authenticated) { - enc.extension_info_awaiters - .entry(extension_name) - .or_insert(vec![]) - .push(reply_channel); - } - } - } - } - Msg::GetServerSigAlgs { reply_channel } => { - let _ = reply_channel.send(self.server_sig_algs.clone()); - } - Msg::Keepalive { want_reply } => { - let _ = self.send_keepalive(want_reply); - } - Msg::Ping { reply_channel } => { - let _ = self.send_ping(reply_channel); - } - Msg::NoMoreSessions { want_reply } => { - let _ = self.no_more_sessions(want_reply); - } - msg => { - // should be unreachable, since the receiver only gets - // messages from methods implemented within russh - unimplemented!("unimplemented (server-only?) message: {:?}", msg) - } - } - Ok(()) - } - - fn begin_rekey(&mut self) -> Result<(), crate::Error> { - debug!("beginning re-key"); - let mut kex = ClientKex::new( - self.common.config.clone(), - &self.common.config.client_id, - &self.common.remote_sshid, - match &self.common.encrypted { - None => KexCause::Initial, - Some(enc) => KexCause::Rekey { - strict: self.common.strict_kex, - session_id: enc.session_id.clone(), - }, - }, - ); - - kex.kexinit(&mut self.common.packet_writer)?; - self.kex = SessionKexState::InProgress(kex); - Ok(()) - } - - /// Flush the temporary cleartext buffer into the encryption - /// buffer. This does *not* flush to the socket. - fn flush(&mut self) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if enc.flush( - &self.common.config.as_ref().limits, - &mut self.common.packet_writer, - )? && !self.kex.active() - { - self.begin_rekey()?; - } - } - Ok(()) - } - - /// Immediately trigger a session re-key after flushing all pending packets - pub fn initiate_rekey(&mut self) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - enc.rekey_wanted = true; - self.flush()? - } - Ok(()) - } -} - -async fn reply( - session: &mut Session, - handler: &mut H, - kex_done_signal: &mut Option>, - pkt: &mut IncomingSshPacket, -) -> Result<(), H::Error> { - if let Some(message_type) = pkt.buffer.first() { - debug!( - "< msg type {message_type:?}, seqn {:?}, len {}", - pkt.seqn.0, - pkt.buffer.len() - ); - if session.common.strict_kex && session.common.encrypted.is_none() { - let seqno = pkt.seqn.0 - 1; // was incremented after read() - validate_server_msg_strict_kex(*message_type, seqno as usize)?; - } - - if [msg::IGNORE, msg::UNIMPLEMENTED, msg::DEBUG].contains(message_type) { - return Ok(()); - } - } - - if pkt.buffer.first() == Some(&msg::KEXINIT) && session.kex == SessionKexState::Idle { - // Not currently in a rekey but received KEXINIT - debug!("server has initiated re-key"); - session.begin_rekey()?; - // Kex will consume the packet right away - } - - let is_kex_msg = pkt.buffer.first().cloned().map(is_kex_msg).unwrap_or(false); - - if is_kex_msg { - if let SessionKexState::InProgress(kex) = session.kex.take() { - let progress = kex.step(Some(pkt), &mut session.common.packet_writer)?; - - match progress { - KexProgress::NeedsReply { kex, reset_seqn } => { - debug!("kex impl continues: {kex:?}"); - session.kex = SessionKexState::InProgress(kex); - if reset_seqn { - debug!("kex impl requests seqno reset"); - session.common.reset_seqn(); - } - } - KexProgress::Done { - server_host_key, - newkeys, - } => { - debug!("kex impl has completed"); - session.common.strict_kex = - session.common.strict_kex || newkeys.names.strict_kex(); - - // Call the kex_done handler before consuming newkeys - let shared_secret = newkeys.kex.shared_secret_bytes(); - handler - .kex_done(shared_secret, &newkeys.names, session) - .await?; - - if session.common.encrypted.is_some() { - // This is a rekey - { - let common = &mut session.common; - common.newkeys(newkeys); - common.packet_writer.buffer().bytes = 0; - if let Some(enc) = common.encrypted.as_mut() { - enc.last_rekey = Instant::now(); - enc.flush_all_pending_with_writer(&mut common.packet_writer)?; - } - } - - let mut pending = std::mem::take(&mut session.pending_reads); - for p in pending.drain(..) { - session.process_packet(handler, &p).await?; - } - session.pending_reads = pending; - session.pending_len = 0; - } else { - // This is the initial kex - if let Some(server_host_key) = &server_host_key { - let check = handler.check_server_key(server_host_key).await?; - if !check { - return Err(crate::Error::UnknownKey.into()); - } - } - - session - .common - .encrypted(initial_encrypted_state(session), newkeys); - - if let Some(sender) = kex_done_signal.take() { - sender.send(()).unwrap_or(()); - } - } - - session.kex = SessionKexState::Idle; - - if session.common.strict_kex { - pkt.seqn = Wrapping(0); - } - - debug!("kex done"); - } - } - - session.flush()?; - - return Ok(()); - } - } - - session.client_read_encrypted(handler, pkt).await -} - -#[cfg(test)] -mod tests { - use std::collections::{HashMap, VecDeque}; - #[cfg(feature = "flate2")] - use std::io::Write; - use std::num::Wrapping; - use std::sync::Arc; - - use ssh_encoding::Encode; - use tokio::sync::mpsc::channel; - use tokio::sync::mpsc::unbounded_channel; - - use super::*; - use crate::auth::{AuthRequest, Method}; - use crate::compression::{Compression, Decompress}; - use crate::kex::{KEXES, NONE}; - use crate::session::{CommonSession, Encrypted, EncryptedState, Exchange}; - use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer}; - use crate::{CryptoVec, cipher, mac}; - - struct TestHandler; - - impl Handler for TestHandler { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _: &ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - } - - fn keyboard_interactive_session() -> ( - Session, - tokio::sync::mpsc::Sender, - tokio::sync::mpsc::UnboundedReceiver, - ) { - let config = Arc::new(Config::default()); - let (sender, receiver) = channel(config.channel_buffer_size); - let (reply_sender, reply_receiver) = unbounded_channel(); - let auth_request = AuthRequest::new(&Method::KeyboardInteractive { - submethods: String::new(), - }); - let session = Session::new( - config.window_size, - CommonSession { - auth_user: "user".to_owned(), - auth_attempts: 0, - auth_method: Some(Method::KeyboardInteractive { - submethods: String::new(), - }), - remote_to_local: Box::new(cipher::clear::Key), - encrypted: Some(Encrypted { - state: EncryptedState::WaitingAuthRequest(auth_request), - exchange: Some(Exchange::default()), - kex: KEXES.get(&NONE).unwrap().make(), - key: 0, - client_mac: mac::NONE, - server_mac: mac::NONE, - session_id: CryptoVec::new(), - channels: HashMap::new(), - last_channel_id: Wrapping(0), - write: Vec::new(), - write_cursor: 0, - last_rekey: russh_util::time::Instant::now(), - server_compression: Compression::None, - client_compression: Compression::None, - decompress: Decompress::None, - rekey_wanted: false, - received_extensions: Vec::new(), - extension_info_awaiters: HashMap::new(), - }), - config, - wants_reply: false, - disconnected: false, - buffer: Vec::new(), - strict_kex: false, - alive_timeouts: 0, - received_data: false, - remote_sshid: b"SSH-2.0-test".to_vec(), - packet_writer: PacketWriter::clear(), - }, - receiver, - reply_sender, - ); - (session, sender, reply_receiver) - } - - #[cfg(feature = "flate2")] - fn authenticated_session() -> Session { - let config = Arc::new(Config::default()); - let (receiver_sender, receiver) = channel(config.channel_buffer_size); - let (reply_sender, _) = unbounded_channel(); - let mut session = Session::new( - config.window_size, - CommonSession { - auth_user: String::new(), - auth_attempts: 0, - auth_method: None, - remote_to_local: Box::new(cipher::clear::Key), - encrypted: Some(Encrypted { - state: EncryptedState::Authenticated, - exchange: Some(Exchange::default()), - kex: KEXES.get(&NONE).unwrap().make(), - key: 0, - client_mac: mac::NONE, - server_mac: mac::NONE, - session_id: CryptoVec::new(), - channels: HashMap::new(), - last_channel_id: Wrapping(0), - write: Vec::new(), - write_cursor: 0, - last_rekey: russh_util::time::Instant::now(), - server_compression: Compression::None, - client_compression: Compression::None, - decompress: Decompress::Zlib(flate2::Decompress::new(true)), - rekey_wanted: false, - received_extensions: Vec::new(), - extension_info_awaiters: HashMap::new(), - }), - config, - wants_reply: false, - disconnected: false, - buffer: Vec::new(), - strict_kex: false, - alive_timeouts: 0, - received_data: false, - remote_sshid: b"SSH-2.0-test".to_vec(), - packet_writer: PacketWriter::clear(), - }, - receiver, - reply_sender, - ); - session.open_global_requests = VecDeque::new(); - let _ = receiver_sender; - session - } - - fn oversized_prompt_count_packet() -> Vec { - let mut packet = Vec::new(); - msg::USERAUTH_INFO_REQUEST_OR_USERAUTH_PK_OK - .encode(&mut packet) - .unwrap(); - "name".encode(&mut packet).unwrap(); - "instructions".encode(&mut packet).unwrap(); - "".encode(&mut packet).unwrap(); - u32::MAX.encode(&mut packet).unwrap(); - packet - } - - #[tokio::test] - async fn oversized_keyboard_interactive_prompt_count_is_rejected() { - let (mut session, _sender, mut replies) = keyboard_interactive_session(); - let mut handler = TestHandler; - let err = session - .process_packet(&mut handler, &oversized_prompt_count_packet()) - .await - .expect_err("malformed prompt count must fail"); - - assert!(matches!(err, crate::Error::Inconsistent)); - assert!( - replies.try_recv().is_err(), - "malformed packet must not emit a reply" - ); - } - - #[cfg(feature = "flate2")] - fn compressed_debug_payload(payload_len: usize) -> Vec { - let mut payload = vec![b'A'; payload_len]; - payload[0] = crate::msg::DEBUG; - - let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best()); - encoder.write_all(&payload).unwrap(); - let compressed = encoder.finish().unwrap(); - assert!(compressed.len() < 256 * 1024); - compressed - } - - #[cfg(feature = "flate2")] - fn incoming_packet(compressed: Vec) -> SSHBuffer { - let mut buffer = SSHBuffer::new(); - buffer.buffer.extend_from_slice(&[0; 5]); - buffer.buffer.extend_from_slice(&compressed); - buffer - } - - #[cfg(feature = "flate2")] - #[tokio::test] - async fn compressed_debug_is_ignored_after_client_parses_it() { - let mut session = authenticated_session(); - let mut handler = TestHandler; - let mut kex_done_signal = None; - let buffer = incoming_packet(compressed_debug_payload(200 * 1024)); - let mut pkt: IncomingSshPacket = session.maybe_decompress(&buffer).unwrap(); - - super::reply(&mut session, &mut handler, &mut kex_done_signal, &mut pkt) - .await - .unwrap(); - - assert!(!session.common.disconnected); - } - - #[cfg(feature = "flate2")] - #[test] - fn oversized_compressed_debug_is_rejected_before_client_ignores_it() { - let mut session = authenticated_session(); - let oversized = crate::cipher::MAXIMUM_DECOMPRESSED_PACKET_LEN + 1024; - let buffer = incoming_packet(compressed_debug_payload(oversized)); - - let err = session.maybe_decompress(&buffer).unwrap_err(); - assert!( - matches!(err, crate::Error::PacketSize(len) if len > crate::cipher::MAXIMUM_DECOMPRESSED_PACKET_LEN) - ); - } -} - -fn initial_encrypted_state(session: &Session) -> EncryptedState { - if session.common.config.anonymous { - EncryptedState::Authenticated - } else { - EncryptedState::WaitingAuthServiceRequest { - accepted: false, - sent: false, - } - } -} - -/// Parameters for dynamic group Diffie-Hellman key exchanges. -#[derive(Debug, Clone)] -pub struct GexParams { - /// Minimum DH group size (in bits) - min_group_size: usize, - /// Preferred DH group size (in bits) - preferred_group_size: usize, - /// Maximum DH group size (in bits) - max_group_size: usize, -} - -impl GexParams { - pub fn new( - min_group_size: usize, - preferred_group_size: usize, - max_group_size: usize, - ) -> Result { - Self::for_client_config(min_group_size, preferred_group_size, max_group_size) - } - - pub fn for_client_config( - min_group_size: usize, - preferred_group_size: usize, - max_group_size: usize, - ) -> Result { - Self::build( - min_group_size, - preferred_group_size, - max_group_size, - ValidationKind::ClientConfig, - ) - } - - fn validate(&self, kind: ValidationKind) -> Result<(), Error> { - match kind { - ValidationKind::ClientConfig => { - if self.min_group_size < 2048 { - return Err(Error::InvalidConfig(format!( - "min_group_size must be at least 2048 bits. We got {} bits", - self.min_group_size - ))); - } - } - ValidationKind::PeerRequest => { - if self.max_group_size < 2048 { - return Err(Error::InvalidConfig(format!( - "max_group_size must be at least 2048 bits. We got {} bits", - self.max_group_size - ))); - } - } - } - - if self.preferred_group_size < self.min_group_size { - return Err(Error::InvalidConfig(format!( - "preferred_group_size must be at least as large as min_group_size. We have preferred_group_size = {} < min_group_size = {}", - self.preferred_group_size, self.min_group_size - ))); - } - if self.max_group_size < self.preferred_group_size { - return Err(Error::InvalidConfig(format!( - "max_group_size must be at least as large as preferred_group_size. We have max_group_size = {} < preferred_group_size = {}", - self.max_group_size, self.preferred_group_size - ))); - } - - Ok(()) - } - - pub(crate) fn from_peer_request( - min_group_size: usize, - preferred_group_size: usize, - max_group_size: usize, - ) -> Result { - Self::build( - min_group_size, - preferred_group_size, - max_group_size, - ValidationKind::PeerRequest, - ) - } - - fn build( - min_group_size: usize, - preferred_group_size: usize, - max_group_size: usize, - kind: ValidationKind, - ) -> Result { - let this = Self { - min_group_size, - preferred_group_size, - max_group_size, - }; - this.validate(kind)?; - Ok(this) - } - - pub fn min_group_size(&self) -> usize { - self.min_group_size - } - - pub fn preferred_group_size(&self) -> usize { - self.preferred_group_size - } - - pub fn max_group_size(&self) -> usize { - self.max_group_size - } -} - -#[derive(Clone, Copy, Eq, PartialEq)] -enum ValidationKind { - ClientConfig, - PeerRequest, -} - -impl Default for GexParams { - fn default() -> GexParams { - GexParams { - min_group_size: 3072, - preferred_group_size: 8192, - max_group_size: 8192, - } - } -} - -/// The configuration of clients. -#[derive(Debug)] -pub struct Config { - /// The client ID string sent at the beginning of the protocol. - pub client_id: SshId, - /// The bytes and time limits before key re-exchange. - pub limits: Limits, - /// The initial size of a channel (used for flow control). - pub window_size: u32, - /// The maximal size of a single packet. - pub maximum_packet_size: u32, - /// Buffer size for each channel (a number of unprocessed messages to store before propagating backpressure to the TCP stream) - pub channel_buffer_size: usize, - /// Lists of preferred algorithms. - pub preferred: negotiation::Preferred, - /// Time after which the connection is garbage-collected. - pub inactivity_timeout: Option, - /// If nothing is received from the server for this amount of time, send a keepalive message. - pub keepalive_interval: Option, - /// If this many keepalives have been sent without reply, close the connection. - pub keepalive_max: usize, - /// Whether to expect and wait for an authentication call. - pub anonymous: bool, - /// DH dynamic group exchange parameters. - pub gex: GexParams, - /// If active, invoke `set_nodelay(true)` on the ssh socket; disabled by default (i.e. Nagle's algorithm is active). - pub nodelay: bool, -} - -impl Default for Config { - fn default() -> Config { - Config { - client_id: SshId::Standard(Cow::Borrowed(concat!( - "SSH-2.0-", - env!("CARGO_PKG_NAME"), - "_", - env!("CARGO_PKG_VERSION") - ))), - limits: Limits::default(), - window_size: 2097152, - maximum_packet_size: 32768, - channel_buffer_size: 100, - preferred: Default::default(), - inactivity_timeout: None, - keepalive_interval: None, - keepalive_max: 3, - anonymous: false, - gex: Default::default(), - nodelay: false, - } - } -} - -/// A client handler. Note that messages can be received from the -/// server at any time during a session. -/// -/// You must at the very least implement the `check_server_key` fn. -/// The default implementation rejects all keys. -/// -/// Note: this is an async trait. The trait functions return `impl Future`, -/// and you can simply define them as `async fn` instead. -#[cfg_attr(feature = "async-trait", async_trait::async_trait)] -pub trait Handler: Sized + Send { - type Error: From + Send + core::fmt::Debug; - - /// Called when the server sends us an authentication banner. This - /// is usually meant to be shown to the user, see - /// [RFC4252](https://tools.ietf.org/html/rfc4252#section-5.4) for - /// more details. - #[allow(unused_variables)] - fn auth_banner( - &mut self, - banner: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called to check the server's public key. This is a very important - /// step to help prevent man-in-the-middle attacks. The default - /// implementation rejects all keys. - #[allow(unused_variables)] - fn check_server_key( - &mut self, - server_public_key: &ssh_key::PublicKey, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Called when key exchange has completed. - /// - /// This callback provides access to the raw shared secret from the KEX, - /// which is useful for protocols that derive additional keys from the - /// SSH shared secret (e.g., for secondary encrypted channels). - /// - /// The `names` parameter contains all negotiated algorithms (kex, cipher, mac, etc.). - /// - /// **Security Warning:** The shared secret is sensitive cryptographic material. - /// Handle it with care and zero it after use if stored. - /// - /// # Arguments - /// - /// * `kex_algorithm` - Name of the key exchange algorithm used - /// * `shared_secret` - The raw shared secret bytes from the key exchange. - /// For some algorithms (like `none`), this may be `None`. - /// * `names` - The negotiated algorithm names - /// * `session` - The current session - #[allow(unused_variables)] - fn kex_done( - &mut self, - shared_secret: Option<&[u8]>, - names: &negotiation::Names, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server confirmed our request to open a - /// channel. A channel can only be written to after receiving this - /// message (this library panics otherwise). - #[allow(unused_variables)] - fn channel_open_confirmation( - &mut self, - id: ChannelId, - max_packet_size: u32, - window_size: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server signals success. - #[allow(unused_variables)] - fn channel_success( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server signals failure. - #[allow(unused_variables)] - fn channel_failure( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server closes a channel. - #[allow(unused_variables)] - fn channel_close( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server sends EOF to a channel. - #[allow(unused_variables)] - fn channel_eof( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server rejected our request to open a channel. - #[allow(unused_variables)] - fn channel_open_failure( - &mut self, - channel: ChannelId, - reason: ChannelOpenFailure, - description: &str, - language: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server opens a channel for a new remote port forwarding connection - #[allow(unused_variables)] - fn server_channel_open_forwarded_tcpip( - &mut self, - channel: Channel, - connected_address: &str, - connected_port: u32, - originator_address: &str, - originator_port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - // Called when the server opens a channel for a new remote UDS forwarding connection - #[allow(unused_variables)] - fn server_channel_open_forwarded_streamlocal( - &mut self, - channel: Channel, - socket_path: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server opens an agent forwarding channel - #[allow(unused_variables)] - fn server_channel_open_agent_forward( - &mut self, - channel: Channel, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server attempts to open a channel of unknown type. It may return `true`, - /// if the channel of unknown type should be accepted. In this case, - /// [Handler::server_channel_open_unknown] will be called soon after. If it returns `false`, - /// the channel will not be created and a rejection message will be sent to the server. - #[allow(unused_variables)] - fn should_accept_unknown_server_channel( - &mut self, - id: ChannelId, - channel_type: &str, - ) -> impl Future + Send { - async { false } - } - - /// Called when the server opens an unknown channel. - #[allow(unused_variables)] - fn server_channel_open_unknown( - &mut self, - channel: Channel, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server opens a session channel. - #[allow(unused_variables)] - fn server_channel_open_session( - &mut self, - channel: Channel, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server opens a direct tcp/ip channel (non-standard). - #[allow(unused_variables)] - fn server_channel_open_direct_tcpip( - &mut self, - channel: Channel, - host_to_connect: &str, - port_to_connect: u32, - originator_address: &str, - originator_port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server opens a direct-streamlocal channel (non-standard). - #[allow(unused_variables)] - fn server_channel_open_direct_streamlocal( - &mut self, - channel: Channel, - socket_path: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server opens an X11 channel. - #[allow(unused_variables)] - fn server_channel_open_x11( - &mut self, - channel: Channel, - originator_address: &str, - originator_port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server sends us data. The `extended_code` - /// parameter is a stream identifier, `None` is usually the - /// standard output, and `Some(1)` is the standard error. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-5.2). - #[allow(unused_variables)] - fn data( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the server sends us data. The `extended_code` - /// parameter is a stream identifier, `None` is usually the - /// standard output, and `Some(1)` is the standard error. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-5.2). - #[allow(unused_variables)] - fn extended_data( - &mut self, - channel: ChannelId, - ext: u32, - data: &[u8], - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The server informs this client of whether the client may - /// perform control-S/control-Q flow control. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.8). - #[allow(unused_variables)] - fn xon_xoff( - &mut self, - channel: ChannelId, - client_can_do: bool, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The remote process has exited, with the given exit status. - #[allow(unused_variables)] - fn exit_status( - &mut self, - channel: ChannelId, - exit_status: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The remote process exited upon receiving a signal. - #[allow(unused_variables)] - fn exit_signal( - &mut self, - channel: ChannelId, - signal_name: Sig, - core_dumped: bool, - error_message: &str, - lang_tag: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the network window is adjusted, meaning that we - /// can send more bytes. This is useful if this client wants to - /// send huge amounts of data, for instance if we have called - /// `Session::data` before, and it returned less than the - /// full amount of data. - #[allow(unused_variables)] - fn window_adjusted( - &mut self, - channel: ChannelId, - new_size: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when this client adjusts the network window. Return the - /// next target window and maximum packet size. - #[allow(unused_variables)] - fn adjust_window(&mut self, channel: ChannelId, window: u32) -> u32 { - window - } - - /// Called when the server signals success. - #[allow(unused_variables)] - fn openssh_ext_host_keys_announced( - &mut self, - keys: Vec, - session: &mut Session, - ) -> impl Future> + Send { - async move { - debug!("openssh_ext_hostkeys_announced: {keys:?}"); - Ok(()) - } - } - - /// Called when the server sent a disconnect message - /// - /// If reason is an Error, this function should re-return the error so the join can also evaluate it - #[allow(unused_variables)] - fn disconnected( - &mut self, - reason: DisconnectReason, - ) -> impl Future> + Send { - async { - debug!("disconnected: {reason:?}"); - match reason { - DisconnectReason::ReceivedDisconnect(_) => Ok(()), - DisconnectReason::Error(e) => Err(e), - } - } - } -} diff --git a/crates/bssh-russh/src/client/session.rs b/crates/bssh-russh/src/client/session.rs deleted file mode 100644 index 81597e07..00000000 --- a/crates/bssh-russh/src/client/session.rs +++ /dev/null @@ -1,541 +0,0 @@ -use log::error; -use ssh_encoding::Encode; -use tokio::sync::oneshot; - -use crate::client::Session; -use crate::session::EncryptedState; -use crate::{map_err, msg, ChannelId, Disconnect, Pty, Sig}; - -impl Session { - fn channel_open_generic( - &mut self, - kind: &[u8], - write_suffix: F, - ) -> Result - where - F: FnOnce(&mut Vec) -> Result<(), crate::Error>, - { - let result = if let Some(ref mut enc) = self.common.encrypted { - match enc.state { - EncryptedState::Authenticated => { - let sender_channel = enc.new_channel( - self.common.config.window_size, - self.common.config.maximum_packet_size, - ); - push_packet!(enc.write, { - msg::CHANNEL_OPEN.encode(&mut enc.write)?; - kind.encode(&mut enc.write)?; - - // sender channel id. - sender_channel.encode(&mut enc.write)?; - - // window. - self.common - .config - .as_ref() - .window_size - .encode(&mut enc.write)?; - - // max packet size. - self.common - .config - .as_ref() - .maximum_packet_size - .encode(&mut enc.write)?; - - write_suffix(&mut enc.write)?; - }); - sender_channel - } - _ => return Err(crate::Error::NotAuthenticated), - } - } else { - return Err(crate::Error::Inconsistent); - }; - Ok(result) - } - - pub fn channel_open_session(&mut self) -> Result { - self.channel_open_generic(b"session", |_| Ok(())) - } - - pub fn channel_open_x11( - &mut self, - originator_address: &str, - originator_port: u32, - ) -> Result { - self.channel_open_generic(b"x11", |write| { - map_err!(originator_address.encode(write))?; - map_err!(originator_port.encode(write))?; // sender channel id. - Ok(()) - }) - } - - pub fn channel_open_direct_tcpip( - &mut self, - host_to_connect: &str, - port_to_connect: u32, - originator_address: &str, - originator_port: u32, - ) -> Result { - self.channel_open_generic(b"direct-tcpip", |write| { - host_to_connect.encode(write)?; - port_to_connect.encode(write)?; // sender channel id. - originator_address.encode(write)?; - originator_port.encode(write)?; // sender channel id. - Ok(()) - }) - } - - pub fn channel_open_direct_streamlocal( - &mut self, - socket_path: &str, - ) -> Result { - self.channel_open_generic(b"direct-streamlocal@openssh.com", |write| { - socket_path.encode(write)?; - "".encode(write)?; // reserved - 0u32.encode(write)?; // reserved - Ok(()) - }) - } - - #[allow(clippy::too_many_arguments)] - pub fn request_pty( - &mut self, - channel: ChannelId, - want_reply: bool, - term: &str, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - terminal_modes: &[(Pty, u32)], - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - map_err!(msg::CHANNEL_REQUEST.encode(&mut enc.write))?; - - channel.recipient_channel.encode(&mut enc.write)?; - "pty-req".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - - term.encode(&mut enc.write)?; - col_width.encode(&mut enc.write)?; - row_height.encode(&mut enc.write)?; - pix_width.encode(&mut enc.write)?; - pix_height.encode(&mut enc.write)?; - - ((1 + 5 * terminal_modes.len()) as u32).encode(&mut enc.write)?; - for &(code, value) in terminal_modes { - if code == Pty::TTY_OP_END { - continue; - } - (code as u8).encode(&mut enc.write)?; - value.encode(&mut enc.write)?; - } - (Pty::TTY_OP_END as u8).encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn request_x11( - &mut self, - channel: ChannelId, - want_reply: bool, - single_connection: bool, - x11_authentication_protocol: &str, - x11_authentication_cookie: &str, - x11_screen_number: u32, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "x11-req".encode(&mut enc.write)?; - enc.write.push(want_reply as u8); - enc.write.push(single_connection as u8); - x11_authentication_protocol.encode(&mut enc.write)?; - x11_authentication_cookie.encode(&mut enc.write)?; - x11_screen_number.encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn set_env( - &mut self, - channel: ChannelId, - want_reply: bool, - variable_name: &str, - variable_value: &str, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "env".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - variable_name.encode(&mut enc.write)?; - variable_value.encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn request_shell( - &mut self, - want_reply: bool, - channel: ChannelId, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "shell".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn exec( - &mut self, - channel: ChannelId, - want_reply: bool, - command: &[u8], - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "exec".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - command.encode(&mut enc.write)?; - }); - return Ok(()); - } - } - error!("exec"); - Ok(()) - } - - pub fn signal(&mut self, channel: ChannelId, signal: Sig) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - channel.recipient_channel.encode(&mut enc.write)?; - "signal".encode(&mut enc.write)?; - 0u8.encode(&mut enc.write)?; - signal.name().encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn request_subsystem( - &mut self, - want_reply: bool, - channel: ChannelId, - name: &str, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "subsystem".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - name.encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn window_change( - &mut self, - channel: ChannelId, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "window-change".encode(&mut enc.write)?; - 0u8.encode(&mut enc.write)?; - col_width.encode(&mut enc.write)?; - row_height.encode(&mut enc.write)?; - pix_width.encode(&mut enc.write)?; - pix_height.encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - /// Requests a TCP/IP forwarding from the server - /// - /// If `reply_channel` is not None, sets want_reply and returns the server's response via the channel, - /// [`Some`] for a success message with port, or [`None`] for failure - pub fn tcpip_forward( - &mut self, - reply_channel: Option>>, - address: &str, - port: u32, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - let want_reply = reply_channel.is_some(); - if let Some(reply_channel) = reply_channel { - self.open_global_requests.push_back( - crate::session::GlobalRequestResponse::TcpIpForward(reply_channel), - ); - } - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "tcpip-forward".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - address.encode(&mut enc.write)?; - port.encode(&mut enc.write)?; - }); - } - Ok(()) - } - - /// Requests cancellation of TCP/IP forwarding from the server - /// - /// If `reply_channel` is not None, sets want_reply and returns the server's response via the channel, - /// `true` for a success message, or `false` for failure - pub fn cancel_tcpip_forward( - &mut self, - reply_channel: Option>, - address: &str, - port: u32, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - let want_reply = reply_channel.is_some(); - if let Some(reply_channel) = reply_channel { - self.open_global_requests.push_back( - crate::session::GlobalRequestResponse::CancelTcpIpForward(reply_channel), - ); - } - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "cancel-tcpip-forward".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - address.encode(&mut enc.write)?; - port.encode(&mut enc.write)?; - }); - } - Ok(()) - } - - /// Requests a UDS forwarding from the server, `socket path` being the server side socket path. - /// - /// If `reply_channel` is not None, sets want_reply and returns the server's response via the channel, - /// `true` for a success message, or `false` for failure - pub fn streamlocal_forward( - &mut self, - reply_channel: Option>, - socket_path: &str, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - let want_reply = reply_channel.is_some(); - if let Some(reply_channel) = reply_channel { - self.open_global_requests.push_back( - crate::session::GlobalRequestResponse::StreamLocalForward(reply_channel), - ); - } - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "streamlocal-forward@openssh.com".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - socket_path.encode(&mut enc.write)?; - }); - } - Ok(()) - } - - /// Requests cancellation of UDS forwarding from the server - /// - /// If `reply_channel` is not None, sets want_reply and returns the server's response via the channel, - /// `true` for a success message and `false` for failure. - pub fn cancel_streamlocal_forward( - &mut self, - reply_channel: Option>, - socket_path: &str, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - let want_reply = reply_channel.is_some(); - if let Some(reply_channel) = reply_channel { - self.open_global_requests.push_back( - crate::session::GlobalRequestResponse::CancelStreamLocalForward(reply_channel), - ); - } - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "cancel-streamlocal-forward@openssh.com".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - socket_path.encode(&mut enc.write)?; - }); - } - Ok(()) - } - - pub fn send_keepalive(&mut self, want_reply: bool) -> Result<(), crate::Error> { - self.open_global_requests - .push_back(crate::session::GlobalRequestResponse::Keepalive); - if let Some(ref mut enc) = self.common.encrypted { - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "keepalive@openssh.com".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - }); - } - Ok(()) - } - - pub fn send_ping(&mut self, reply_channel: oneshot::Sender<()>) -> Result<(), crate::Error> { - self.open_global_requests - .push_back(crate::session::GlobalRequestResponse::Ping(reply_channel)); - if let Some(ref mut enc) = self.common.encrypted { - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "keepalive@openssh.com".encode(&mut enc.write)?; - (true as u8).encode(&mut enc.write)?; - }); - } - Ok(()) - } - - pub fn no_more_sessions(&mut self, want_reply: bool) -> Result<(), crate::Error> { - self.open_global_requests - .push_back(crate::session::GlobalRequestResponse::NoMoreSessions); - if let Some(ref mut enc) = self.common.encrypted { - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "no-more-sessions@openssh.com".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - }); - } - Ok(()) - } - - pub fn data(&mut self, channel: ChannelId, data: impl Into) -> Result<(), crate::Error> { - let is_rekeying = self.kex.active(); - let common = &mut self.common; - if let Some(enc) = common.encrypted.as_mut() { - enc.data_with_writer(&mut common.packet_writer, channel, data, is_rekeying) - } else { - unreachable!() - } - } - - pub fn eof(&mut self, channel: ChannelId) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - enc.eof(channel) - } else { - unreachable!() - } - } - - pub fn close(&mut self, channel: ChannelId) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - enc.close(channel) - } else { - unreachable!() - } - } - - pub fn extended_data( - &mut self, - channel: ChannelId, - ext: u32, - data: impl Into, - ) -> Result<(), crate::Error> { - let is_rekeying = self.kex.active(); - let common = &mut self.common; - if let Some(enc) = common.encrypted.as_mut() { - enc.extended_data_with_writer(&mut common.packet_writer, channel, ext, data, is_rekeying) - } else { - unreachable!() - } - } - - pub fn agent_forward( - &mut self, - channel: ChannelId, - want_reply: bool, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - channel.recipient_channel.encode(&mut enc.write)?; - "auth-agent-req@openssh.com".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - }); - } - } - Ok(()) - } - - pub fn disconnect( - &mut self, - reason: Disconnect, - description: &str, - language_tag: &str, - ) -> Result<(), crate::Error> { - self.common.disconnect(reason, description, language_tag) - } - - pub fn has_pending_data(&self, channel: ChannelId) -> bool { - if let Some(ref enc) = self.common.encrypted { - enc.has_pending_data(channel) - } else { - false - } - } - - pub fn sender_window_size(&self, channel: ChannelId) -> usize { - if let Some(ref enc) = self.common.encrypted { - enc.sender_window_size(channel) - } else { - 0 - } - } - - /// Returns the SSH ID (Protocol Version + Software Version) the server sent when connecting - /// - /// This should contain only ASCII characters for implementations conforming to RFC4253, Section 4.2: - /// - /// > Both the 'protoversion' and 'softwareversion' strings MUST consist of - /// > printable US-ASCII characters, with the exception of whitespace - /// > characters and the minus sign (-). - /// - /// So it usually is fine to convert it to a `String` using `String::from_utf8_lossy` - pub fn remote_sshid(&self) -> &[u8] { - &self.common.remote_sshid - } -} diff --git a/crates/bssh-russh/src/client/test.rs b/crates/bssh-russh/src/client/test.rs deleted file mode 100644 index 94069a12..00000000 --- a/crates/bssh-russh/src/client/test.rs +++ /dev/null @@ -1,161 +0,0 @@ -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::sync::{Arc, Mutex}; - - use log::debug; - use ssh_key::PrivateKey; - use tokio::net::TcpListener; - - // Import client types directly since we're in the client module - use crate::client::{Config, Handler, connect}; - use crate::keys::PrivateKeyWithHashAlg; - use rand::rng; - use crate::server::{self, Auth, Handler as ServerHandler, Server, Session}; - use crate::{ChannelId, SshId}; // Import directly from crate root - use crate::Error; - - #[derive(Clone)] - struct TestServer { - clients: Arc>>, - id: usize, - } - - impl server::Server for TestServer { - type Handler = Self; - - fn new_client(&mut self, _: Option) -> Self { - let s = self.clone(); - self.id += 1; - s - } - } - - impl ServerHandler for TestServer { - type Error = Error; - - async fn channel_open_session( - &mut self, - channel: crate::channels::Channel, - session: &mut Session, - ) -> Result { - { - let mut clients = self.clients.lock().unwrap(); - clients.insert((self.id, channel.id()), session.handle()); - } - Ok(true) - } - - async fn auth_publickey( - &mut self, - _: &str, - _: &ssh_key::PublicKey, - ) -> Result { - debug!("auth_publickey"); - Ok(Auth::Accept) - } - - async fn data( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> Result<(), Self::Error> { - debug!("server received data: {:?}", std::str::from_utf8(data)); - session.data(channel, data.to_vec())?; - Ok(()) - } - } - - struct Client {} - - impl Handler for Client { - type Error = Error; - - async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result { - Ok(true) - } - } - - #[tokio::test] - async fn test_client_connects_to_protocol_1_99() { - let _ = env_logger::try_init(); - - // Create a client key - let client_key = PrivateKey::random(&mut rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Configure the server - let mut config = server::Config::default(); - config.auth_rejection_time = std::time::Duration::from_secs(1); - config.server_id = SshId::Standard("SSH-1.99-CustomServer_1.0".into()); - config.inactivity_timeout = None; - config - .keys - .push(PrivateKey::random(&mut rng(), ssh_key::Algorithm::Ed25519).unwrap()); - let config = Arc::new(config); - - // Create server struct - let mut server = TestServer { - clients: Arc::new(Mutex::new(HashMap::new())), - id: 0, - }; - - // Start the TCP listener for our mock server - let socket = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = socket.local_addr().unwrap(); - - // Spawn a separate task that will handle the server connection - tokio::spawn(async move { - // Accept a connection - let (socket, _) = socket.accept().await.unwrap(); - - // Handle the connection with the server - let server_handler = server.new_client(None); - server::run_stream(config, socket, server_handler) - .await - .unwrap(); - }); - - println!("Server listening on {addr}"); - - // Configure the client - let client_config = Arc::new(Config::default()); - - // Connect to the server - let mut session = connect(client_config, addr, Client {}).await.unwrap(); - - // Unfortunately, we can't directly verify the protocol version from the client API - // The Protocol199Stream wrapper ensures the server sends SSH-1.99-CustomServer_1.0 - // The test passing means the client accepted this protocol version - - // Try to authenticate - let auth_result = session - .authenticate_publickey( - std::env::var("USER").unwrap_or("user".to_string()), - PrivateKeyWithHashAlg::new( - Arc::new(client_key), - session.best_supported_rsa_hash().await.unwrap().flatten(), - ), - ) - .await - .unwrap(); - - assert!(auth_result.success()); - - // Try opening a session channel - let mut channel = session.channel_open_session().await.unwrap(); - - // Send some data - let test_data = b"Hello, 1.99 protocol server!"; - channel.data(&test_data[..]).await.unwrap(); - - // Wait for response - let msg = channel.wait().await.unwrap(); - match msg { - crate::channels::ChannelMsg::Data { data: msg_data } => { - assert_eq!(test_data.as_slice(), &msg_data[..]); - } - msg => panic!("Unexpected message {msg:?}"), - } - } -} diff --git a/crates/bssh-russh/src/compression.rs b/crates/bssh-russh/src/compression.rs deleted file mode 100644 index 1500623d..00000000 --- a/crates/bssh-russh/src/compression.rs +++ /dev/null @@ -1,355 +0,0 @@ -use std::convert::TryFrom; - -use delegate::delegate; -use ssh_encoding::Encode; - -#[cfg(feature = "flate2")] -use crate::cipher::MAXIMUM_DECOMPRESSED_PACKET_LEN; - -#[derive(Debug, Clone)] -pub enum Compression { - None, - #[cfg(feature = "flate2")] - Zlib, - #[cfg(feature = "flate2")] - ZlibOpenSSH, -} - -#[derive(Debug)] -pub enum Compress { - None, - #[cfg(feature = "flate2")] - Zlib(flate2::Compress), -} - -#[derive(Debug)] -pub enum Decompress { - None, - #[cfg(feature = "flate2")] - Zlib(flate2::Decompress), -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub struct Name(&'static str); -impl AsRef for Name { - fn as_ref(&self) -> &str { - self.0 - } -} - -impl Encode for Name { - delegate! { to self.as_ref() { - fn encoded_len(&self) -> Result; - fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error>; - }} -} - -impl TryFrom<&str> for Name { - type Error = (); - fn try_from(s: &str) -> Result { - ALL_COMPRESSION_ALGORITHMS - .iter() - .find(|x| x.0 == s) - .map(|x| **x) - .ok_or(()) - } -} - -pub const NONE: Name = Name("none"); -#[cfg(feature = "flate2")] -pub const ZLIB: Name = Name("zlib"); -#[cfg(feature = "flate2")] -pub const ZLIB_LEGACY: Name = Name("zlib@openssh.com"); - -pub const ALL_COMPRESSION_ALGORITHMS: &[&Name] = &[ - &NONE, - #[cfg(feature = "flate2")] - &ZLIB, - #[cfg(feature = "flate2")] - &ZLIB_LEGACY, -]; - -#[cfg(feature = "flate2")] -impl Compression { - pub fn new(name: &Name) -> Self { - if name == &ZLIB { - Compression::Zlib - } else if name == &ZLIB_LEGACY { - Compression::ZlibOpenSSH - } else { - Compression::None - } - } - - pub fn init_compress(&self, comp: &mut Compress) { - match *self { - Compression::Zlib | Compression::ZlibOpenSSH => { - if let Compress::Zlib(ref mut c) = *comp { - c.reset() - } else { - *comp = - Compress::Zlib(flate2::Compress::new(flate2::Compression::fast(), true)) - } - } - Compression::None => { - *comp = Compress::None; - } - } - } - - pub fn init_decompress(&self, comp: &mut Decompress) { - match *self { - Compression::Zlib | Compression::ZlibOpenSSH => { - if let Decompress::Zlib(ref mut c) = *comp { - c.reset(true) - } else { - *comp = Decompress::Zlib(flate2::Decompress::new(true)) - } - } - Compression::None => { - *comp = Decompress::None; - } - } - } -} - -impl Compression { - /// Returns true if compression should be deferred until after authentication. - /// "zlib@openssh.com" defers; RFC 4253 "zlib" does not. - pub fn is_deferred(&self) -> bool { - match self { - #[cfg(feature = "flate2")] - Compression::ZlibOpenSSH => true, - _ => false, - } - } -} - -#[cfg(not(feature = "flate2"))] -impl Compression { - pub fn new(_name: &Name) -> Self { - Compression::None - } - - pub fn init_compress(&self, _: &mut Compress) {} - - pub fn init_decompress(&self, _: &mut Decompress) {} -} - -#[cfg(not(feature = "flate2"))] -impl Compress { - pub fn compress<'a>( - &mut self, - input: &'a [u8], - _: &'a mut Vec, - ) -> Result<&'a [u8], crate::Error> { - Ok(input) - } - - pub fn compress_into( - &mut self, - input: &[u8], - output: &mut Vec, - start_len: usize, - ) -> Result { - output.truncate(start_len); - output.extend_from_slice(input); - Ok(input.len()) - } -} - -#[cfg(not(feature = "flate2"))] -impl Decompress { - pub fn decompress<'a>( - &mut self, - input: &'a [u8], - _: &'a mut Vec, - ) -> Result<&'a [u8], crate::Error> { - Ok(input) - } -} - -#[cfg(all(test, feature = "flate2"))] -mod tests { - use std::io::Write; - - use flate2::write::ZlibEncoder; - - use super::*; - - #[test] - fn decompressed_packet_at_limit_is_accepted() { - let payload = vec![b'A'; MAXIMUM_DECOMPRESSED_PACKET_LEN]; - let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::best()); - encoder.write_all(&payload).unwrap(); - let compressed = encoder.finish().unwrap(); - - let mut decompressor = Decompress::Zlib(flate2::Decompress::new(true)); - let mut output = Vec::new(); - - let decompressed = decompressor.decompress(&compressed, &mut output).unwrap(); - assert_eq!(decompressed.len(), MAXIMUM_DECOMPRESSED_PACKET_LEN); - } - - #[test] - fn oversized_decompressed_packet_is_rejected() { - let payload = vec![b'A'; MAXIMUM_DECOMPRESSED_PACKET_LEN + 1024]; - let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::best()); - encoder.write_all(&payload).unwrap(); - let compressed = encoder.finish().unwrap(); - - let mut decompressor = Decompress::Zlib(flate2::Decompress::new(true)); - let mut output = Vec::new(); - - let err = decompressor.decompress(&compressed, &mut output).unwrap_err(); - assert!( - matches!(err, crate::Error::PacketSize(len) if len > MAXIMUM_DECOMPRESSED_PACKET_LEN) - ); - } - - #[test] - fn empty_compressed_packet_does_not_spin() { - let compressed = Vec::new(); - let mut decompressor = Decompress::Zlib(flate2::Decompress::new(true)); - let mut output = Vec::new(); - - let decompressed = decompressor.decompress(&compressed, &mut output).unwrap(); - assert!(decompressed.is_empty()); - } -} - -#[cfg(feature = "flate2")] -impl Compress { - fn zlib_output_reserve_bound(input_len: usize) -> usize { - input_len.saturating_add(10) - } - - pub fn compress<'a>( - &mut self, - input: &'a [u8], - output: &'a mut Vec, - ) -> Result<&'a [u8], crate::Error> { - match *self { - Compress::None => Ok(input), - Compress::Zlib(ref mut z) => { - output.clear(); - let n_in = z.total_in() as usize; - let n_out = z.total_out() as usize; - output.resize(input.len() + 10, 0); - let flush = flate2::FlushCompress::Partial; - loop { - let n_in_ = z.total_in() as usize - n_in; - let n_out_ = z.total_out() as usize - n_out; - #[allow(clippy::indexing_slicing)] // length checked - let c = z.compress(&input[n_in_..], &mut output[n_out_..], flush)?; - match c { - flate2::Status::BufError => { - output.resize(output.len() * 2, 0); - } - _ => break, - } - } - let n_out_ = z.total_out() as usize - n_out; - #[allow(clippy::indexing_slicing)] // length checked - Ok(&output[..n_out_]) - } - } - } - - pub fn compress_into( - &mut self, - input: &[u8], - output: &mut Vec, - start_len: usize, - ) -> Result { - match *self { - Compress::None => { - output.truncate(start_len); - output.extend_from_slice(input); - Ok(input.len()) - } - Compress::Zlib(ref mut z) => { - output.truncate(start_len); - let n_in = z.total_in() as usize; - let n_out = z.total_out() as usize; - let reserve = Self::zlib_output_reserve_bound(input.len()); - output.resize(start_len + reserve, 0); - let flush = flate2::FlushCompress::Partial; - loop { - let n_in_ = z.total_in() as usize - n_in; - let n_out_ = z.total_out() as usize - n_out; - #[allow(clippy::indexing_slicing)] // length checked - let c = z.compress(&input[n_in_..], &mut output[start_len + n_out_..], flush)?; - match c { - flate2::Status::BufError => { - let growth = output.len().saturating_sub(start_len).max(1); - output.resize(output.len() + growth, 0); - } - _ => break, - } - } - let n_out_ = z.total_out() as usize - n_out; - output.truncate(start_len + n_out_); - Ok(n_out_) - } - } - } -} - -#[cfg(feature = "flate2")] -impl Decompress { - pub fn decompress<'a>( - &mut self, - input: &'a [u8], - output: &'a mut Vec, - ) -> Result<&'a [u8], crate::Error> { - match *self { - Decompress::None => Ok(input), - Decompress::Zlib(ref mut z) => { - output.clear(); - let n_in = z.total_in() as usize; - let n_out = z.total_out() as usize; - let max_output_len = MAXIMUM_DECOMPRESSED_PACKET_LEN - .checked_add(1) - .ok_or(crate::Error::PacketSize(usize::MAX))?; - output.resize(input.len().clamp(1, max_output_len), 0); - let flush = flate2::FlushDecompress::None; - loop { - let n_in_ = z.total_in() as usize - n_in; - let n_out_ = z.total_out() as usize - n_out; - #[allow(clippy::indexing_slicing)] // length checked - let d = z.decompress(&input[n_in_..], &mut output[n_out_..], flush); - match d? { - flate2::Status::Ok | flate2::Status::BufError => { - let consumed_all_input = n_in_ == input.len(); - let output_full = n_out_ == output.len(); - - if !output_full && consumed_all_input { - break; - } - - if output_full { - if output.len() == max_output_len { - break; - } - let next_len = output - .len() - .checked_mul(2) - .map(|len| len.min(max_output_len)) - .ok_or(crate::Error::PacketSize(usize::MAX))?; - output.resize(next_len, 0); - } - } - _ => break, - } - } - let n_out_ = z.total_out() as usize - n_out; - if n_out_ > MAXIMUM_DECOMPRESSED_PACKET_LEN { - return Err(crate::Error::PacketSize(n_out_)); - } - #[allow(clippy::indexing_slicing)] // length checked - Ok(&output[..n_out_]) - } - } - } -} diff --git a/crates/bssh-russh/src/helpers.rs b/crates/bssh-russh/src/helpers.rs deleted file mode 100644 index d961183e..00000000 --- a/crates/bssh-russh/src/helpers.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::fmt::Debug; - -use ssh_encoding::{Decode, Encode}; - -#[doc(hidden)] -pub trait EncodedExt { - fn encoded(&self) -> ssh_key::Result>; -} - -impl EncodedExt for E { - fn encoded(&self) -> ssh_key::Result> { - let mut buf = Vec::new(); - self.encode(&mut buf)?; - Ok(buf) - } -} - -mod limited_string { - use super::*; - use std::ops::Deref; - - pub struct LimitedString(String); - - impl Deref for LimitedString { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl Decode for LimitedString { - type Error = ssh_encoding::Error; - - fn decode(reader: &mut impl ssh_encoding::Reader) -> Result { - reader.read_prefixed(|reader| { - let len = reader.remaining_len(); - if len > N { - return Err(ssh_encoding::Error::Length); - } - - // Allocate only after the SSH string length has been bounded. - let mut buf = vec![0; len]; - reader.read(&mut buf)?; - let value = - String::from_utf8(buf).map_err(|_| ssh_encoding::Error::CharacterEncoding)?; - reader.ensure_finished()?; - - Ok(Self(value)) - }) - } - } -} - -mod name_list { - use std::ops::Deref; - - use super::*; - const MAX_NAME_LIST_ENTRIES: usize = 1024; - const MAX_NAME_LIST_BYTES: usize = 16 * 1024; - - pub struct NameList(pub Vec); - - impl Debug for NameList { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl Deref for NameList { - type Target = [String]; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl NameList { - pub fn as_encoded_string(&self) -> String { - self.0.join(",") - } - - pub fn from_encoded_string(value: &str) -> Result { - // RFC 4251 §5: a name-list may have zero names (a string of - // zero length). `"".split(',')` yields one empty element, - // which the per-name validation below would reject. - if value.is_empty() { - return Ok(Self(Vec::new())); - } - Ok(Self(value.split(',').try_fold( - Vec::new(), - |mut list, name| { - if name.is_empty() || !name.is_ascii() { - return Err(ssh_encoding::Error::CharacterEncoding); - } - if list.len() > MAX_NAME_LIST_ENTRIES { - Err(ssh_encoding::Error::Length) - } else { - list.push(name.into()); - Ok(list) - } - }, - )?)) - } - } - - impl Encode for NameList { - fn encoded_len(&self) -> Result { - self.as_encoded_string().encoded_len() - } - - fn encode( - &self, - writer: &mut impl ssh_encoding::Writer, - ) -> Result<(), ssh_encoding::Error> { - self.as_encoded_string().encode(writer) - } - } - - impl Decode for NameList { - fn decode(reader: &mut impl ssh_encoding::Reader) -> Result { - let s = LimitedString::::decode(reader)?; - Self::from_encoded_string(&s) - } - - type Error = ssh_encoding::Error; - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn empty_name_list_is_valid() { - // RFC 4251 §5 permits a zero-name list. Servers that only - // offer AEAD ciphers (e.g. hssh) send empty MAC name-lists. - let nl = NameList::from_encoded_string("").unwrap(); - assert!(nl.0.is_empty()); - } - - #[test] - fn name_list_round_trip() { - let nl = NameList::from_encoded_string("a,b,c").unwrap(); - assert_eq!(nl.0, vec!["a", "b", "c"]); - assert_eq!(nl.as_encoded_string(), "a,b,c"); - } - - #[test] - fn name_list_rejects_empty_entry() { - // An empty entry mid-list (",,") is still invalid — only - // the zero-length whole-list case is allowed. - assert!(NameList::from_encoded_string("a,,b").is_err()); - } - } -} - -pub use limited_string::LimitedString; -pub use name_list::NameList; - -pub(crate) mod macros { - #[allow(clippy::crate_in_macro_def)] - macro_rules! map_err { - ($result:expr) => { - $result.map_err(|e| crate::Error::from(e)) - }; - } - - pub(crate) use map_err; -} - -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] -pub(crate) use macros::map_err; - -#[doc(hidden)] -pub fn sign_with_hash_alg(key: &PrivateKeyWithHashAlg, data: &[u8]) -> ssh_key::Result> { - Ok(match key.key_data() { - #[cfg(feature = "rsa")] - ssh_key::private::KeypairData::Rsa(rsa_keypair) => { - let ssh_key::Algorithm::Rsa { hash } = key.algorithm() else { - unreachable!(); - }; - signature::Signer::try_sign(&(rsa_keypair, hash), data)?.encoded()? - } - keypair => signature::Signer::try_sign(keypair, data)?.encoded()?, - }) -} - -mod algorithm { - use ssh_key::{Algorithm, HashAlg}; - - pub trait AlgorithmExt { - fn hash_alg(&self) -> Option; - fn with_hash_alg(&self, hash_alg: Option) -> Self; - fn new_certificate_ext(algo: &str) -> Result - where - Self: Sized; - } - - impl AlgorithmExt for Algorithm { - fn hash_alg(&self) -> Option { - match self { - Algorithm::Rsa { hash } => *hash, - _ => None, - } - } - - fn with_hash_alg(&self, hash_alg: Option) -> Self { - match self { - Algorithm::Rsa { .. } => Algorithm::Rsa { hash: hash_alg }, - x => x.clone(), - } - } - - fn new_certificate_ext(algo: &str) -> Result { - match algo { - "rsa-sha2-256-cert-v01@openssh.com" => Ok(Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }), - "rsa-sha2-512-cert-v01@openssh.com" => Ok(Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }), - x => Algorithm::new_certificate(x), - } - } - } -} - -#[doc(hidden)] -pub use algorithm::AlgorithmExt; - -use crate::keys::key::PrivateKeyWithHashAlg; diff --git a/crates/bssh-russh/src/kex/curve25519.rs b/crates/bssh-russh/src/kex/curve25519.rs deleted file mode 100644 index 7d888cad..00000000 --- a/crates/bssh-russh/src/kex/curve25519.rs +++ /dev/null @@ -1,175 +0,0 @@ -use byteorder::{BigEndian, ByteOrder}; -use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; -use log::debug; -use sha2::Digest; -use ssh_encoding::{Encode, Writer}; - -use super::{ - KexAlgorithm, KexAlgorithmImplementor, KexType, SharedSecret, compute_keys, encode_mpint, -}; -use crate::mac::{self}; -use crate::session::Exchange; -use crate::{CryptoVec, cipher, msg}; - -pub struct Curve25519KexType {} - -impl KexType for Curve25519KexType { - fn make(&self) -> KexAlgorithm { - Curve25519Kex { - local_secret: None, - shared_secret: None, - } - .into() - } -} - -#[doc(hidden)] -pub struct Curve25519Kex { - local_secret: Option, - shared_secret: Option, -} - -impl std::fmt::Debug for Curve25519Kex { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "Algorithm {{ local_secret: [hidden], shared_secret: [hidden] }}", - ) - } -} - -// We used to support curve "NIST P-256" here, but the security of -// that curve is controversial, see -// http://safecurves.cr.yp.to/rigid.html -impl KexAlgorithmImplementor for Curve25519Kex { - fn skip_exchange(&self) -> bool { - false - } - - #[doc(hidden)] - fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), crate::Error> { - debug!("server_dh"); - - let client_pubkey = { - if payload.first() != Some(&msg::KEX_ECDH_INIT) { - return Err(crate::Error::Inconsistent); - } - - #[allow(clippy::indexing_slicing)] // length checked - let pubkey_len = BigEndian::read_u32(&payload[1..]) as usize; - - if pubkey_len != 32 { - return Err(crate::Error::Kex); - } - - if payload.len() < 5 + pubkey_len { - return Err(crate::Error::Inconsistent); - } - - let mut pubkey = MontgomeryPoint([0; 32]); - #[allow(clippy::indexing_slicing)] // length checked - pubkey.0.clone_from_slice(&payload[5..5 + 32]); - pubkey - }; - - let server_secret = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>()); - let server_pubkey = (ED25519_BASEPOINT_TABLE * &server_secret).to_montgomery(); - - // fill exchange. - exchange.server_ephemeral.clear(); - exchange - .server_ephemeral - .extend_from_slice(&server_pubkey.0); - let shared = server_secret * client_pubkey; - self.shared_secret = Some(shared); - Ok(()) - } - - #[doc(hidden)] - fn client_dh( - &mut self, - client_ephemeral: &mut Vec, - writer: &mut impl Writer, - ) -> Result<(), crate::Error> { - let client_secret = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>()); - let client_pubkey = (ED25519_BASEPOINT_TABLE * &client_secret).to_montgomery(); - - // fill exchange. - client_ephemeral.clear(); - client_ephemeral.extend_from_slice(&client_pubkey.0); - - msg::KEX_ECDH_INIT.encode(writer)?; - (client_pubkey.0[..]).encode(writer)?; - - self.local_secret = Some(client_secret); - Ok(()) - } - - fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> { - let local_secret = self.local_secret.take().ok_or(crate::Error::KexInit)?; - let mut remote_pubkey = MontgomeryPoint([0; 32]); - remote_pubkey.0.clone_from_slice(remote_pubkey_); - let shared = local_secret * remote_pubkey; - self.shared_secret = Some(shared); - Ok(()) - } - - fn shared_secret_bytes(&self) -> Option<&[u8]> { - self.shared_secret.as_ref().map(|s| s.0.as_slice()) - } - - fn compute_exchange_hash( - &self, - key: &[u8], - exchange: &Exchange, - buffer: &mut CryptoVec, - ) -> Result, crate::Error> { - // Computing the exchange hash, see page 7 of RFC 5656. - buffer.clear(); - exchange.client_id.encode(buffer)?; - exchange.server_id.encode(buffer)?; - exchange.client_kex_init.encode(buffer)?; - exchange.server_kex_init.encode(buffer)?; - - buffer.extend(key); - exchange.client_ephemeral.encode(buffer)?; - exchange.server_ephemeral.encode(buffer)?; - - if let Some(ref shared) = self.shared_secret { - encode_mpint(&shared.0, buffer)?; - } - - let mut hasher = sha2::Sha256::new(); - hasher.update(&buffer); - - Ok(hasher.finalize().to_vec()) - } - - fn compute_keys( - &self, - session_id: &[u8], - exchange_hash: &[u8], - cipher: cipher::Name, - remote_to_local_mac: mac::Name, - local_to_remote_mac: mac::Name, - is_server: bool, - ) -> Result { - let shared_secret = self - .shared_secret - .as_ref() - .map(|x| SharedSecret::from_mpint(&x.0)) - .transpose()?; - - compute_keys::( - shared_secret.as_ref(), - session_id, - exchange_hash, - cipher, - remote_to_local_mac, - local_to_remote_mac, - is_server, - ) - } -} diff --git a/crates/bssh-russh/src/kex/dh/groups.rs b/crates/bssh-russh/src/kex/dh/groups.rs deleted file mode 100644 index cb3bcabe..00000000 --- a/crates/bssh-russh/src/kex/dh/groups.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::fmt::Debug; -use std::ops::Deref; - -use hex_literal::hex; -use num_bigint::{BigRng010, BigUint}; - -#[derive(Clone)] -pub enum DhGroupUInt { - Static(&'static [u8]), - Owned(Vec), -} - -impl From> for DhGroupUInt { - fn from(x: Vec) -> Self { - Self::Owned(x) - } -} - -impl DhGroupUInt { - pub const fn new(x: &'static [u8]) -> Self { - Self::Static(x) - } -} - -impl Deref for DhGroupUInt { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - match self { - Self::Static(x) => x, - Self::Owned(x) => x, - } - } -} - -#[derive(Clone)] -pub struct DhGroup { - pub(crate) prime: DhGroupUInt, - pub(crate) generator: DhGroupUInt, - // pub(crate) exp_size: u64, -} - -impl DhGroup { - pub fn bit_size(&self) -> usize { - let Some(fsb_idx) = self.prime.deref().iter().position(|&x| x != 0) else { - return 0; - }; - (self.prime.deref().len() - fsb_idx) * 8 - } -} - -impl Debug for DhGroup { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DhGroup") - .field("prime", &format!("<{} bytes>", self.prime.deref().len())) - .field( - "generator", - &format!("<{} bytes>", self.generator.deref().len()), - ) - .finish() - } -} - -pub const DH_GROUP1: DhGroup = DhGroup { - prime: DhGroupUInt::new( - hex!( - " - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 - 29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD - EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 - E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED - EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381 - FFFFFFFF FFFFFFFF - " - ) - .as_slice(), - ), - generator: DhGroupUInt::new(&[2]), - // exp_size: 256, -}; - -pub const DH_GROUP14: DhGroup = DhGroup { - prime: DhGroupUInt::new( - hex!( - " - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 - 29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD - EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 - E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED - EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D - C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F - 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D - 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B - E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 - DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 - 15728E5A 8AACAA68 FFFFFFFF FFFFFFFF - " - ) - .as_slice(), - ), - generator: DhGroupUInt::new(&[2]), - // exp_size: 256, -}; - -/// https://www.ietf.org/rfc/rfc3526.txt -pub const DH_GROUP15: DhGroup = DhGroup { - prime: DhGroupUInt::new( - hex!( - " - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 - 29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD - EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 - E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED - EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D - C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F - 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D - 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B - E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 - DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 - 15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64 - ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7 - ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B - F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C - BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 - 43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF - " - ) - .as_slice(), - ), - generator: DhGroupUInt::new(&[2]), -}; - -pub const DH_GROUP16: DhGroup = DhGroup { - prime: DhGroupUInt::new( - hex!( - " - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 - 29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD - EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 - E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED - EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D - C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F - 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D - 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B - E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 - DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 - 15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64 - ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7 - ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B - F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C - BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 - 43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 - 88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA - 2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6 - 287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED - 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9 - 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 - FFFFFFFF FFFFFFFF - " - ) - .as_slice(), - ), - generator: DhGroupUInt::new(&[2]), - // exp_size: 512, -}; - -/// https://www.ietf.org/rfc/rfc3526.txt -pub const DH_GROUP17: DhGroup = DhGroup { - prime: DhGroupUInt::new( - hex!( - " - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 - 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B - 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 - A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 - 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 - FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D - 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C - 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 - 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D - 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D - B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 - 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C - BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC - E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 - 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB - 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 - 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 - D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 - 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 - AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 - DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 - 2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 - F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F - BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA - CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B - B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 - 387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E - 6DCC4024 FFFFFFFF FFFFFFFF - " - ) - .as_slice(), - ), - generator: DhGroupUInt::new(&[2]), -}; - -/// https://www.ietf.org/rfc/rfc3526.txt -pub const DH_GROUP18: DhGroup = DhGroup { - prime: DhGroupUInt::new( - hex!( - " - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 - 29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD - EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 - E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED - EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D - C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F - 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D - 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B - E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 - DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 - 15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64 - ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7 - ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B - F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C - BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 - 43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 - 88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA - 2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6 - 287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED - 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9 - 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 - 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD - F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831 - 179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B - DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF - 5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6 - D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3 - 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA - CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 - 06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C - DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE - 12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4 - 38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300 - 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568 - 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9 - 22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B - 4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A - 062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36 - 4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1 - B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92 - 4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47 - 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71 - 60C980DD 98EDD3DF FFFFFFFF FFFFFFFF - " - ) - .as_slice(), - ), - generator: DhGroupUInt::new(&[2]), -}; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub(crate) struct DH { - prime_num: BigUint, - generator: BigUint, - private_key: BigUint, - public_key: BigUint, - shared_secret: BigUint, -} - -impl DH { - pub fn new(group: &DhGroup) -> Self { - Self { - prime_num: BigUint::from_bytes_be(&group.prime), - generator: BigUint::from_bytes_be(&group.generator), - private_key: BigUint::default(), - public_key: BigUint::default(), - shared_secret: BigUint::default(), - } - } - - pub fn generate_private_key(&mut self, is_server: bool) -> BigUint { - let q = (&self.prime_num - &BigUint::from(1u8)) / &BigUint::from(2u8); - let mut rng = rand::rng(); - self.private_key = - rng.random_biguint_range(&if is_server { 1u8.into() } else { 2u8.into() }, &q); - self.private_key.clone() - } - - pub fn generate_public_key(&mut self) -> BigUint { - self.public_key = self.generator.modpow(&self.private_key, &self.prime_num); - self.public_key.clone() - } - - pub fn compute_shared_secret(&mut self, other_public_key: BigUint) -> BigUint { - self.shared_secret = other_public_key.modpow(&self.private_key, &self.prime_num); - self.shared_secret.clone() - } - - pub fn validate_shared_secret(&self, shared_secret: &BigUint) -> bool { - let one = BigUint::from(1u8); - let prime_minus_one = &self.prime_num - &one; - - shared_secret > &one && shared_secret < &prime_minus_one - } - - pub fn decode_public_key(buffer: &[u8]) -> BigUint { - BigUint::from_bytes_be(buffer) - } - - pub fn validate_public_key(&self, public_key: &BigUint) -> bool { - let one = BigUint::from(1u8); - let prime_minus_one = &self.prime_num - &one; - - public_key > &one && public_key < &prime_minus_one - } -} - -pub(crate) const BUILTIN_SAFE_DH_GROUPS: &[&DhGroup] = &[&DH_GROUP14, &DH_GROUP16]; diff --git a/crates/bssh-russh/src/kex/dh/mod.rs b/crates/bssh-russh/src/kex/dh/mod.rs deleted file mode 100644 index 730a2f2e..00000000 --- a/crates/bssh-russh/src/kex/dh/mod.rs +++ /dev/null @@ -1,354 +0,0 @@ -pub mod groups; -use std::marker::PhantomData; - -use byteorder::{BigEndian, ByteOrder}; -use digest::Digest; -use groups::DH; -use log::{error, trace}; -use num_bigint::BigUint; -use sha1::Sha1; -use sha2::{Sha256, Sha512}; -use ssh_encoding::{Decode, Encode, Reader, Writer}; - -use self::groups::{ - DhGroup, DH_GROUP1, DH_GROUP14, DH_GROUP15, DH_GROUP16, DH_GROUP17, DH_GROUP18, -}; -use super::{compute_keys, KexAlgorithm, KexAlgorithmImplementor, KexType, SharedSecret}; -use crate::client::GexParams; -use crate::session::Exchange; -use crate::{cipher, mac, msg, CryptoVec, Error}; - -pub(crate) struct DhGroup15Sha512KexType {} - -impl KexType for DhGroup15Sha512KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP15)).into() - } -} - -pub(crate) struct DhGroup17Sha512KexType {} - -impl KexType for DhGroup17Sha512KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP17)).into() - } -} - -pub(crate) struct DhGroup18Sha512KexType {} - -impl KexType for DhGroup18Sha512KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP18)).into() - } -} - -pub(crate) struct DhGexSha1KexType {} - -impl KexType for DhGexSha1KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(None).into() - } -} - -pub(crate) struct DhGexSha256KexType {} - -impl KexType for DhGexSha256KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(None).into() - } -} - -pub(crate) struct DhGroup1Sha1KexType {} - -impl KexType for DhGroup1Sha1KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP1)).into() - } -} - -pub(crate) struct DhGroup14Sha1KexType {} - -impl KexType for DhGroup14Sha1KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP14)).into() - } -} - -pub(crate) struct DhGroup14Sha256KexType {} - -impl KexType for DhGroup14Sha256KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP14)).into() - } -} - -pub(crate) struct DhGroup16Sha512KexType {} - -impl KexType for DhGroup16Sha512KexType { - fn make(&self) -> KexAlgorithm { - DhGroupKex::::new(Some(&DH_GROUP16)).into() - } -} - -#[doc(hidden)] -pub(crate) struct DhGroupKex { - dh: Option, - shared_secret: Option>, - is_dh_gex: bool, - _digest: PhantomData, -} - -impl DhGroupKex { - pub(crate) fn new(group: Option<&DhGroup>) -> DhGroupKex { - DhGroupKex { - dh: group.map(DH::new), - shared_secret: None, - is_dh_gex: group.is_none(), - _digest: PhantomData, - } - } -} - -impl std::fmt::Debug for DhGroupKex { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "Algorithm {{ local_secret: [hidden], shared_secret: [hidden] }}", - ) - } -} - -pub(crate) fn biguint_to_mpint(biguint: &BigUint) -> Vec { - let mut mpint = Vec::new(); - let bytes = biguint.to_bytes_be(); - if let Some(b) = bytes.first() { - if b > &0x7f { - mpint.push(0); - } - } - mpint.extend(&bytes); - mpint -} - -impl KexAlgorithmImplementor for DhGroupKex { - fn skip_exchange(&self) -> bool { - false - } - - fn is_dh_gex(&self) -> bool { - self.is_dh_gex - } - - fn client_dh_gex_init( - &mut self, - gex: &GexParams, - writer: &mut impl Writer, - ) -> Result<(), Error> { - msg::KEX_DH_GEX_REQUEST.encode(writer)?; - (gex.min_group_size() as u32).encode(writer)?; - (gex.preferred_group_size() as u32).encode(writer)?; - (gex.max_group_size() as u32).encode(writer)?; - Ok(()) - } - - #[allow(dead_code)] - fn dh_gex_set_group(&mut self, group: DhGroup) -> Result<(), crate::Error> { - self.dh = Some(DH::new(&group)); - Ok(()) - } - - #[doc(hidden)] - fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), Error> { - let Some(dh) = self.dh.as_mut() else { - error!("DH kex sequence error, dh is None in server_dh"); - return Err(Error::Inconsistent); - }; - - let client_pubkey = { - if payload.first() != Some(&msg::KEX_ECDH_INIT) - && payload.first() != Some(&msg::KEX_DH_GEX_INIT) - { - return Err(Error::Inconsistent); - } - - #[allow(clippy::indexing_slicing)] // length checked - let pubkey_len = BigEndian::read_u32(&payload[1..]) as usize; - - if payload.len() < 5 + pubkey_len { - return Err(Error::Inconsistent); - } - - &payload - .get(5..(5 + pubkey_len)) - .ok_or(Error::Inconsistent)? - }; - - trace!("client_pubkey: {client_pubkey:?}"); - - dh.generate_private_key(true); - let server_pubkey = &dh.generate_public_key(); - if !dh.validate_public_key(server_pubkey) { - return Err(Error::Inconsistent); - } - - let encoded_server_pubkey = biguint_to_mpint(server_pubkey); - - // fill exchange. - exchange.server_ephemeral.clear(); - exchange.server_ephemeral.extend_from_slice(&encoded_server_pubkey); - - let decoded_client_pubkey = DH::decode_public_key(client_pubkey); - if !dh.validate_public_key(&decoded_client_pubkey) { - return Err(Error::Inconsistent); - } - - let shared = dh.compute_shared_secret(decoded_client_pubkey); - if !dh.validate_shared_secret(&shared) { - return Err(Error::Inconsistent); - } - self.shared_secret = Some(biguint_to_mpint(&shared)); - Ok(()) - } - - #[doc(hidden)] - fn client_dh( - &mut self, - client_ephemeral: &mut Vec, - writer: &mut impl Writer, - ) -> Result<(), Error> { - let Some(dh) = self.dh.as_mut() else { - error!("DH kex sequence error, dh is None in client_dh"); - return Err(Error::Inconsistent); - }; - - dh.generate_private_key(false); - let client_pubkey = &dh.generate_public_key(); - - if !dh.validate_public_key(client_pubkey) { - return Err(Error::Inconsistent); - } - - // fill exchange. - let encoded_pubkey = biguint_to_mpint(client_pubkey); - client_ephemeral.clear(); - client_ephemeral.extend_from_slice(&encoded_pubkey); - - if self.is_dh_gex { - msg::KEX_DH_GEX_INIT.encode(writer)?; - } else { - msg::KEX_ECDH_INIT.encode(writer)?; - } - - encoded_pubkey.encode(writer)?; - - Ok(()) - } - - fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), Error> { - let Some(dh) = self.dh.as_mut() else { - error!("DH kex sequence error, dh is None in compute_shared_secret"); - return Err(Error::Inconsistent); - }; - - let remote_pubkey = DH::decode_public_key(remote_pubkey_); - - if !dh.validate_public_key(&remote_pubkey) { - return Err(Error::Inconsistent); - } - - let shared = dh.compute_shared_secret(remote_pubkey); - if !dh.validate_shared_secret(&shared) { - return Err(Error::Inconsistent); - } - self.shared_secret = Some(biguint_to_mpint(&shared)); - Ok(()) - } - - fn shared_secret_bytes(&self) -> Option<&[u8]> { - self.shared_secret.as_deref() - } - - fn compute_exchange_hash( - &self, - key: &[u8], - exchange: &Exchange, - buffer: &mut CryptoVec, - ) -> Result, Error> { - // Computing the exchange hash, see page 7 of RFC 5656. - buffer.clear(); - exchange.client_id.encode(buffer)?; - exchange.server_id.encode(buffer)?; - exchange.client_kex_init.encode(buffer)?; - exchange.server_kex_init.encode(buffer)?; - - buffer.extend(key); - - if let Some((gex_params, dh_group)) = &exchange.gex { - gex_params.encode(buffer)?; - biguint_to_mpint(&BigUint::from_bytes_be(&dh_group.prime)).encode(buffer)?; - biguint_to_mpint(&BigUint::from_bytes_be(&dh_group.generator)).encode(buffer)?; - } - - exchange.client_ephemeral.encode(buffer)?; - exchange.server_ephemeral.encode(buffer)?; - - if let Some(ref shared) = self.shared_secret { - shared.encode(buffer)?; - } - - let mut hasher = D::new(); - hasher.update(&buffer); - - Ok(hasher.finalize().to_vec()) - } - - fn compute_keys( - &self, - session_id: &[u8], - exchange_hash: &[u8], - cipher: cipher::Name, - remote_to_local_mac: mac::Name, - local_to_remote_mac: mac::Name, - is_server: bool, - ) -> Result { - let shared_secret = self - .shared_secret - .as_deref() - .map(SharedSecret::from_mpint) - .transpose()?; - - compute_keys::( - shared_secret.as_ref(), - session_id, - exchange_hash, - cipher, - remote_to_local_mac, - local_to_remote_mac, - is_server, - ) - } -} - -impl Encode for GexParams { - fn encoded_len(&self) -> Result { - Ok(0u32.encoded_len()? * 3) - } - - fn encode(&self, writer: &mut impl Writer) -> Result<(), ssh_encoding::Error> { - (self.min_group_size() as u32).encode(writer)?; - (self.preferred_group_size() as u32).encode(writer)?; - (self.max_group_size() as u32).encode(writer)?; - Ok(()) - } -} - -impl Decode for GexParams { - fn decode(reader: &mut impl Reader) -> Result { - let min_group_size = u32::decode(reader)? as usize; - let preferred_group_size = u32::decode(reader)? as usize; - let max_group_size = u32::decode(reader)? as usize; - GexParams::from_peer_request(min_group_size, preferred_group_size, max_group_size) - } - - type Error = Error; -} diff --git a/crates/bssh-russh/src/kex/ecdh_nistp.rs b/crates/bssh-russh/src/kex/ecdh_nistp.rs deleted file mode 100644 index 335f2c64..00000000 --- a/crates/bssh-russh/src/kex/ecdh_nistp.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::marker::PhantomData; -use std::ops::Deref; - -use byteorder::{BigEndian, ByteOrder}; -use elliptic_curve::Generate; -use elliptic_curve::ecdh::{EphemeralSecret, SharedSecret}; -use elliptic_curve::point::PointCompression; -use elliptic_curve::sec1::{FromSec1Point, ModulusSize, ToSec1Point}; -use elliptic_curve::{AffinePoint, Curve, CurveArithmetic, FieldBytesSize}; -use log::debug; -use p256::NistP256; -use p384::NistP384; -use p521::NistP521; -use sha2::{Digest, Sha256, Sha384, Sha512}; -use ssh_encoding::{Encode, Writer}; - -use super::{KexAlgorithm, SharedSecret as KexSharedSecret, encode_mpint}; -use crate::kex::{KexAlgorithmImplementor, KexType, compute_keys}; -use crate::mac::{self}; -use crate::session::Exchange; -use crate::{CryptoVec, cipher, msg}; - -pub struct EcdhNistP256KexType {} - -impl KexType for EcdhNistP256KexType { - fn make(&self) -> KexAlgorithm { - EcdhNistPKex:: { - local_secret: None, - shared_secret: None, - _digest: PhantomData, - } - .into() - } -} - -pub struct EcdhNistP384KexType {} - -impl KexType for EcdhNistP384KexType { - fn make(&self) -> KexAlgorithm { - EcdhNistPKex:: { - local_secret: None, - shared_secret: None, - _digest: PhantomData, - } - .into() - } -} - -pub struct EcdhNistP521KexType {} - -impl KexType for EcdhNistP521KexType { - fn make(&self) -> KexAlgorithm { - EcdhNistPKex:: { - local_secret: None, - shared_secret: None, - _digest: PhantomData, - } - .into() - } -} - -#[doc(hidden)] -pub struct EcdhNistPKex { - local_secret: Option>, - shared_secret: Option>, - _digest: PhantomData, -} - -impl std::fmt::Debug for EcdhNistPKex { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "Algorithm {{ local_secret: [hidden], shared_secret: [hidden] }}", - ) - } -} - -impl KexAlgorithmImplementor for EcdhNistPKex -where - C: PointCompression, - FieldBytesSize: ModulusSize, - AffinePoint: FromSec1Point + ToSec1Point, -{ - fn skip_exchange(&self) -> bool { - false - } - - #[doc(hidden)] - fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), crate::Error> { - debug!("server_dh"); - - let client_pubkey = { - if payload.first() != Some(&msg::KEX_ECDH_INIT) { - return Err(crate::Error::Inconsistent); - } - - #[allow(clippy::indexing_slicing)] // length checked - let pubkey_len = BigEndian::read_u32(&payload[1..]) as usize; - - if payload.len() < 5 + pubkey_len { - return Err(crate::Error::Inconsistent); - } - - #[allow(clippy::indexing_slicing)] // length checked - elliptic_curve::PublicKey::::from_sec1_bytes(&payload[5..(5 + pubkey_len)]) - .map_err(|_| crate::Error::Inconsistent)? - }; - - let server_secret = elliptic_curve::ecdh::EphemeralSecret::::generate_from_rng(&mut rand::rng()); - let server_pubkey = server_secret.public_key(); - - // fill exchange. - exchange.server_ephemeral.clear(); - exchange - .server_ephemeral - .extend_from_slice(&server_pubkey.to_sec1_bytes()); - let shared = server_secret.diffie_hellman(&client_pubkey); - self.shared_secret = Some(shared); - Ok(()) - } - - #[doc(hidden)] - fn client_dh( - &mut self, - client_ephemeral: &mut Vec, - writer: &mut impl Writer, - ) -> Result<(), crate::Error> { - let client_secret = elliptic_curve::ecdh::EphemeralSecret::::generate_from_rng(&mut rand::rng()); - let client_pubkey = client_secret.public_key(); - - // fill exchange. - client_ephemeral.clear(); - client_ephemeral.extend_from_slice(&client_pubkey.to_sec1_bytes()); - - msg::KEX_ECDH_INIT.encode(writer)?; - client_pubkey.to_sec1_bytes().encode(writer)?; - - self.local_secret = Some(client_secret); - Ok(()) - } - - fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> { - let local_secret = self.local_secret.take().ok_or(crate::Error::KexInit)?; - let pubkey = elliptic_curve::PublicKey::::from_sec1_bytes(remote_pubkey_) - .map_err(|_| crate::Error::KexInit)?; - self.shared_secret = Some(local_secret.diffie_hellman(&pubkey)); - Ok(()) - } - - fn shared_secret_bytes(&self) -> Option<&[u8]> { - self.shared_secret - .as_ref() - .map(|s| s.raw_secret_bytes().deref()) - } - - fn compute_exchange_hash( - &self, - key: &[u8], - exchange: &Exchange, - buffer: &mut CryptoVec, - ) -> Result, crate::Error> { - // Computing the exchange hash, see page 7 of RFC 5656. - buffer.clear(); - exchange.client_id.deref().encode(buffer)?; - exchange.server_id.deref().encode(buffer)?; - exchange.client_kex_init.deref().encode(buffer)?; - exchange.server_kex_init.deref().encode(buffer)?; - - buffer.extend(key); - exchange.client_ephemeral.deref().encode(buffer)?; - exchange.server_ephemeral.deref().encode(buffer)?; - - if let Some(ref shared) = self.shared_secret { - encode_mpint(shared.raw_secret_bytes(), buffer)?; - } - - let mut hasher = D::new(); - hasher.update(&buffer); - - Ok(hasher.finalize().to_vec()) - } - - fn compute_keys( - &self, - session_id: &[u8], - exchange_hash: &[u8], - cipher: cipher::Name, - remote_to_local_mac: mac::Name, - local_to_remote_mac: mac::Name, - is_server: bool, - ) -> Result { - let shared_secret = self - .shared_secret - .as_ref() - .map(|x| KexSharedSecret::from_mpint(x.raw_secret_bytes())) - .transpose()?; - - compute_keys::( - shared_secret.as_ref(), - session_id, - exchange_hash, - cipher, - remote_to_local_mac, - local_to_remote_mac, - is_server, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_shared_secret() { - let mut party1 = EcdhNistPKex:: { - local_secret: Some(EphemeralSecret::::generate_from_rng(&mut rand::rng())), - shared_secret: None, - _digest: PhantomData, - }; - let p1_pubkey = party1.local_secret.as_ref().unwrap().public_key(); - - let mut party2 = EcdhNistPKex:: { - local_secret: Some(EphemeralSecret::::generate_from_rng(&mut rand::rng())), - shared_secret: None, - _digest: PhantomData, - }; - let p2_pubkey = party2.local_secret.as_ref().unwrap().public_key(); - - party1 - .compute_shared_secret(&p2_pubkey.to_sec1_bytes()) - .unwrap(); - - party2 - .compute_shared_secret(&p1_pubkey.to_sec1_bytes()) - .unwrap(); - - let p1_shared_secret = party1.shared_secret.unwrap(); - let p2_shared_secret = party2.shared_secret.unwrap(); - - assert_eq!( - p1_shared_secret.raw_secret_bytes(), - p2_shared_secret.raw_secret_bytes() - ) - } -} diff --git a/crates/bssh-russh/src/kex/hybrid_mlkem.rs b/crates/bssh-russh/src/kex/hybrid_mlkem.rs deleted file mode 100644 index fd3fd2ad..00000000 --- a/crates/bssh-russh/src/kex/hybrid_mlkem.rs +++ /dev/null @@ -1,434 +0,0 @@ -use byteorder::{BigEndian, ByteOrder}; -use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; -use log::debug; -use ml_kem::Kem; -use ml_kem::{ - MlKem768, - kem::{Decapsulate, DecapsulationKey, Encapsulate, EncapsulationKey}, - KeyExport, TryKeyInit, -}; -use sha2::Digest; -use ssh_encoding::{Encode, Writer}; - -use super::{KexAlgorithm, KexAlgorithmImplementor, KexType, SharedSecret, compute_keys}; -use crate::mac; -use crate::session::Exchange; -use crate::{CryptoVec, Error, cipher, msg}; - -const MLKEM768_PUBLIC_KEY_SIZE: usize = 1184; -const MLKEM768_CIPHERTEXT_SIZE: usize = 1088; -const X25519_PUBLIC_KEY_SIZE: usize = 32; - -type MlKem768PublicKey = EncapsulationKey; -type MlKem768PrivateKey = DecapsulationKey; -type MlKem768Ciphertext = ml_kem::Ciphertext; - -pub struct MlKem768X25519KexType {} - -impl KexType for MlKem768X25519KexType { - fn make(&self) -> KexAlgorithm { - MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - } - .into() - } -} - -#[doc(hidden)] -pub struct MlKem768X25519Kex { - mlkem_secret: Option>, - x25519_secret: Option, - k_pq: Option, - k_cl: Option, -} - -impl std::fmt::Debug for MlKem768X25519Kex { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "MlKem768X25519Kex {{ mlkem_secret: [hidden], x25519_secret: [hidden], k_pq: [hidden], k_cl: [hidden] }}", - ) - } -} - -impl KexAlgorithmImplementor for MlKem768X25519Kex { - fn skip_exchange(&self) -> bool { - false - } - - fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), Error> { - debug!("server_dh (hybrid ML-KEM)"); - - if payload.first() != Some(&msg::KEX_HYBRID_INIT) { - return Err(Error::Inconsistent); - } - - #[allow(clippy::indexing_slicing)] - let c_init_len = BigEndian::read_u32(&payload[1..]) as usize; - - if payload.len() < 5 + c_init_len { - return Err(Error::Inconsistent); - } - - if c_init_len != MLKEM768_PUBLIC_KEY_SIZE + X25519_PUBLIC_KEY_SIZE { - return Err(Error::Kex); - } - - #[allow(clippy::indexing_slicing)] - let c_init = &payload[5..5 + c_init_len]; - - #[allow(clippy::indexing_slicing)] - let c_pk2_bytes = &c_init[..MLKEM768_PUBLIC_KEY_SIZE]; - #[allow(clippy::indexing_slicing)] - let c_pk1_bytes = &c_init[MLKEM768_PUBLIC_KEY_SIZE..]; - - let c_pk2 = MlKem768PublicKey::new_from_slice(c_pk2_bytes).map_err(|_| Error::Kex)?; - - let mut c_pk1 = MontgomeryPoint([0; 32]); - c_pk1.0.copy_from_slice(c_pk1_bytes); - - let (s_ct2, k_pq_shared_secret) = c_pk2.encapsulate_with_rng(&mut rand::rng()); - - let s_secret = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>()); - let s_pk1 = (ED25519_BASEPOINT_TABLE * &s_secret).to_montgomery(); - - let k_cl = s_secret * c_pk1; - - exchange.server_ephemeral.clear(); - exchange - .server_ephemeral - .extend_from_slice(s_ct2.as_slice()); - exchange.server_ephemeral.extend_from_slice(&s_pk1.0); - - self.k_pq = Some(k_pq_shared_secret); - self.k_cl = Some(k_cl); - - Ok(()) - } - - fn client_dh( - &mut self, - client_ephemeral: &mut Vec, - writer: &mut impl Writer, - ) -> Result<(), Error> { - let (mlkem_sk, mlkem_pk) = MlKem768::generate_keypair_from_rng(&mut rand::rng()); - - let x25519_secret = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>()); - let x25519_pk = (ED25519_BASEPOINT_TABLE * &x25519_secret).to_montgomery(); - - client_ephemeral.clear(); - client_ephemeral.extend(&mlkem_pk.to_bytes()); - client_ephemeral.extend(&x25519_pk.0); - - msg::KEX_HYBRID_INIT.encode(writer)?; - let mut c_init = Vec::::new(); - c_init.extend(mlkem_pk.to_bytes()); - c_init.extend(&x25519_pk.0); - c_init.as_slice().encode(writer)?; - - self.mlkem_secret = Some(Box::new(mlkem_sk)); - self.x25519_secret = Some(x25519_secret); - - Ok(()) - } - - fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), Error> { - if remote_pubkey_.len() != MLKEM768_CIPHERTEXT_SIZE + X25519_PUBLIC_KEY_SIZE { - return Err(Error::Kex); - } - - #[allow(clippy::indexing_slicing)] - let s_ct2_bytes = &remote_pubkey_[..MLKEM768_CIPHERTEXT_SIZE]; - #[allow(clippy::indexing_slicing)] - let s_pk1_bytes = &remote_pubkey_[MLKEM768_CIPHERTEXT_SIZE..]; - - let s_ct2 = MlKem768Ciphertext::try_from(s_ct2_bytes).map_err(|_| Error::KexInit)?; - - let mlkem_secret = self.mlkem_secret.take().ok_or(Error::KexInit)?; - let k_pq_shared_secret = mlkem_secret.decapsulate(&s_ct2); - let mut s_pk1 = MontgomeryPoint([0; 32]); - s_pk1.0.copy_from_slice(s_pk1_bytes); - - let x25519_secret = self.x25519_secret.take().ok_or(Error::KexInit)?; - let k_cl = x25519_secret * s_pk1; - - self.k_pq = Some(k_pq_shared_secret); - self.k_cl = Some(k_cl); - - Ok(()) - } - - fn shared_secret_bytes(&self) -> Option<&[u8]> { - // For hybrid KEX, the shared secret is a combination of ML-KEM and X25519. - // The actual combined secret is computed during compute_keys. - // We return the X25519 portion as that's what's directly available. - // Users needing the full hybrid secret should use compute_keys. - self.k_cl.as_ref().map(|k| k.0.as_slice()) - } - - fn compute_exchange_hash( - &self, - key: &[u8], - exchange: &Exchange, - buffer: &mut CryptoVec, - ) -> Result, Error> { - buffer.clear(); - exchange.client_id.encode(buffer)?; - exchange.server_id.encode(buffer)?; - exchange.client_kex_init.encode(buffer)?; - exchange.server_kex_init.encode(buffer)?; - - buffer.extend(key); - - exchange.client_ephemeral.encode(buffer)?; - exchange.server_ephemeral.encode(buffer)?; - - let k_pq = self.k_pq.as_ref().ok_or(Error::KexInit)?; - let k_cl = self.k_cl.as_ref().ok_or(Error::KexInit)?; - - let mut combined = Vec::new(); - combined.extend_from_slice(k_pq); - combined.extend_from_slice(&k_cl.0); - - let mut hasher = sha2::Sha256::new(); - hasher.update(&combined); - let k = hasher.finalize(); - - (*k).encode(buffer)?; - - let mut hasher = sha2::Sha256::new(); - hasher.update(&buffer); - - Ok(hasher.finalize().to_vec()) - } - - fn compute_keys( - &self, - session_id: &[u8], - exchange_hash: &[u8], - cipher: cipher::Name, - remote_to_local_mac: mac::Name, - local_to_remote_mac: mac::Name, - is_server: bool, - ) -> Result { - let k_pq = self.k_pq.as_ref().ok_or(Error::KexInit)?; - let k_cl = self.k_cl.as_ref().ok_or(Error::KexInit)?; - - let mut combined = Vec::new(); - combined.extend_from_slice(k_pq); - combined.extend_from_slice(&k_cl.0); - - let mut hasher = sha2::Sha256::new(); - hasher.update(&combined); - let k = hasher.finalize(); - - let shared_secret = SharedSecret::from_string(&k)?; - - compute_keys::( - Some(&shared_secret), - session_id, - exchange_hash, - cipher, - remote_to_local_mac, - local_to_remote_mac, - is_server, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ssh_encoding::Encode; - - #[test] - fn test_mlkem768x25519_key_exchange() { - let mut client_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut server_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut client_ephemeral = Vec::new(); - let mut client_init_msg = Vec::new(); - - client_kex - .client_dh(&mut client_ephemeral, &mut client_init_msg) - .unwrap(); - - assert_eq!( - client_ephemeral.len(), - MLKEM768_PUBLIC_KEY_SIZE + X25519_PUBLIC_KEY_SIZE - ); - assert!(client_kex.mlkem_secret.is_some()); - assert!(client_kex.x25519_secret.is_some()); - - let mut exchange = Exchange::default(); - server_kex - .server_dh(&mut exchange, &client_init_msg) - .unwrap(); - - assert_eq!( - exchange.server_ephemeral.len(), - MLKEM768_CIPHERTEXT_SIZE + X25519_PUBLIC_KEY_SIZE - ); - assert!(server_kex.k_pq.is_some()); - assert!(server_kex.k_cl.is_some()); - - client_kex - .compute_shared_secret(&exchange.server_ephemeral) - .unwrap(); - - assert!(client_kex.k_pq.is_some()); - assert!(client_kex.k_cl.is_some()); - - let client_k_pq = client_kex.k_pq.unwrap(); - let server_k_pq = server_kex.k_pq.unwrap(); - assert_eq!( - client_k_pq, server_k_pq, - "ML-KEM shared secrets should match" - ); - - let client_k_cl = client_kex.k_cl.unwrap(); - let server_k_cl = server_kex.k_cl.unwrap(); - assert_eq!( - client_k_cl.0, server_k_cl.0, - "X25519 shared secrets should match" - ); - } - - #[test] - fn test_mlkem768x25519_exchange_hash() { - let mut client_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut server_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut client_ephemeral = Vec::new(); - let mut client_init_msg = Vec::new(); - client_kex - .client_dh(&mut client_ephemeral, &mut client_init_msg) - .unwrap(); - - let mut exchange = Exchange { - client_id: b"SSH-2.0-Test_Client".to_vec(), - server_id: b"SSH-2.0-Test_Server".to_vec(), - client_kex_init: bytes::Bytes::from_static(b"client_kex_init"), - server_kex_init: bytes::Bytes::from_static(b"server_kex_init"), - client_ephemeral: client_ephemeral.clone(), - server_ephemeral: Vec::new(), - gex: None, - }; - - server_kex - .server_dh(&mut exchange, &client_init_msg) - .unwrap(); - client_kex - .compute_shared_secret(&exchange.server_ephemeral) - .unwrap(); - - let key = b"test_host_key"; - let mut buffer = CryptoVec::new(); - - let client_hash = client_kex - .compute_exchange_hash(key, &exchange, &mut buffer) - .unwrap(); - - let server_hash = server_kex - .compute_exchange_hash(key, &exchange, &mut buffer) - .unwrap(); - - assert_eq!( - client_hash, server_hash, - "Exchange hashes should match between client and server" - ); - assert_eq!(client_hash.len(), 32, "SHA-256 hash should be 32 bytes"); - } - - #[test] - fn test_mlkem768x25519_invalid_ciphertext_length() { - let mut client_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut client_ephemeral = Vec::new(); - let mut client_init_msg = Vec::new(); - client_kex - .client_dh(&mut client_ephemeral, &mut client_init_msg) - .unwrap(); - - let invalid_reply = vec![0u8; 100]; - let result = client_kex.compute_shared_secret(&invalid_reply); - - assert!(result.is_err(), "Should reject invalid ciphertext length"); - } - - #[test] - fn test_mlkem768x25519_invalid_init_length() { - let mut server_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut invalid_init = Vec::new(); - msg::KEX_HYBRID_INIT.encode(&mut invalid_init).unwrap(); - let invalid_data = vec![0u8; 100]; - invalid_data.encode(&mut invalid_init).unwrap(); - - let mut exchange = Exchange::default(); - let result = server_kex.server_dh(&mut exchange, &invalid_init); - - assert!(result.is_err(), "Should reject invalid C_INIT length"); - } - - #[test] - fn test_mlkem768x25519_message_format() { - let mut client_kex = MlKem768X25519Kex { - mlkem_secret: None, - x25519_secret: None, - k_pq: None, - k_cl: None, - }; - - let mut client_ephemeral = Vec::new(); - let mut client_init_msg = Vec::new(); - client_kex - .client_dh(&mut client_ephemeral, &mut client_init_msg) - .unwrap(); - - assert!(client_init_msg.len() > 5, "Message should include header"); - - assert_eq!( - client_init_msg[0], - msg::KEX_HYBRID_INIT, - "First byte should be KEX_HYBRID_INIT" - ); - } -} diff --git a/crates/bssh-russh/src/kex/mod.rs b/crates/bssh-russh/src/kex/mod.rs deleted file mode 100644 index 2da167b0..00000000 --- a/crates/bssh-russh/src/kex/mod.rs +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -//! -//! This module exports kex algorithm names for use with [Preferred]. -mod curve25519; -pub mod dh; -mod ecdh_nistp; -mod hybrid_mlkem; -mod none; -use std::cell::RefCell; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::fmt::Debug; -use std::sync::LazyLock; - -use curve25519::Curve25519KexType; -use delegate::delegate; -use dh::groups::DhGroup; -use dh::{ - DhGexSha1KexType, DhGexSha256KexType, DhGroup1Sha1KexType, DhGroup14Sha1KexType, - DhGroup14Sha256KexType, DhGroup15Sha512KexType, DhGroup16Sha512KexType, DhGroup17Sha512KexType, - DhGroup18Sha512KexType, -}; -use digest::Digest; -use ecdh_nistp::{EcdhNistP256KexType, EcdhNistP384KexType, EcdhNistP521KexType}; -use enum_dispatch::enum_dispatch; -use hybrid_mlkem::MlKem768X25519KexType; -use p256::NistP256; -use p384::NistP384; -use p521::NistP521; -use sha1::Sha1; -use sha2::{Sha256, Sha384, Sha512}; -use ssh_encoding::{Encode, Writer}; -use ssh_key::PublicKey; - -use crate::cipher::CIPHERS; -use crate::client::GexParams; -use crate::mac::{self, MACS}; -use crate::session::{Exchange, NewKeys}; -use crate::{CryptoVec, Error, cipher}; - -#[derive(Debug)] -pub(crate) enum SessionKexState { - Idle, - InProgress(K), - Taken, // some async activity still going on such as host key checks -} - -impl PartialEq for SessionKexState { - fn eq(&self, other: &Self) -> bool { - core::mem::discriminant(self) == core::mem::discriminant(other) - } -} - -impl SessionKexState { - pub fn active(&self) -> bool { - match self { - SessionKexState::Idle => false, - SessionKexState::InProgress(_) => true, - SessionKexState::Taken => true, - } - } - - pub fn take(&mut self) -> Self { - // TODO maybe make this take a guarded closure - std::mem::replace( - self, - match self { - SessionKexState::Idle => SessionKexState::Idle, - _ => SessionKexState::Taken, - }, - ) - } -} - -#[derive(Debug)] -pub(crate) enum KexCause { - Initial, - Rekey { strict: bool, session_id: CryptoVec }, -} - -impl KexCause { - pub fn is_strict_rekey(&self) -> bool { - matches!(self, Self::Rekey { strict: true, .. }) - } - - pub fn is_rekey(&self) -> bool { - match self { - Self::Initial => false, - Self::Rekey { .. } => true, - } - } - - pub fn session_id(&self) -> Option<&CryptoVec> { - match self { - Self::Initial => None, - Self::Rekey { session_id, .. } => Some(session_id), - } - } -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub(crate) enum KexProgress { - NeedsReply { - kex: T, - reset_seqn: bool, - }, - Done { - server_host_key: Option, - newkeys: NewKeys, - }, -} - -#[enum_dispatch(KexAlgorithmImplementor)] -pub(crate) enum KexAlgorithm { - DhGroupKexSha1(dh::DhGroupKex), - DhGroupKexSha256(dh::DhGroupKex), - DhGroupKexSha512(dh::DhGroupKex), - Curve25519Kex(curve25519::Curve25519Kex), - EcdhNistP256Kex(ecdh_nistp::EcdhNistPKex), - EcdhNistP384Kex(ecdh_nistp::EcdhNistPKex), - EcdhNistP521Kex(ecdh_nistp::EcdhNistPKex), - MlKem768X25519Kex(hybrid_mlkem::MlKem768X25519Kex), - None(none::NoneKexAlgorithm), -} - -pub(crate) trait KexType { - fn make(&self) -> KexAlgorithm; -} - -impl Debug for KexAlgorithm { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "KexAlgorithm") - } -} - -#[enum_dispatch] -pub(crate) trait KexAlgorithmImplementor { - fn skip_exchange(&self) -> bool; - fn is_dh_gex(&self) -> bool { - false - } - - #[allow(unused_variables)] - fn client_dh_gex_init( - &mut self, - gex: &GexParams, - writer: &mut impl Writer, - ) -> Result<(), Error> { - Err(Error::KexInit) - } - - #[allow(unused_variables)] - fn dh_gex_set_group(&mut self, group: DhGroup) -> Result<(), Error> { - Err(Error::KexInit) - } - - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), Error>; - - fn client_dh( - &mut self, - client_ephemeral: &mut Vec, - writer: &mut impl Writer, - ) -> Result<(), Error>; - - fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), Error>; - - /// Get the raw shared secret bytes. - /// - /// This is useful for protocols that need to derive additional keys from the - /// SSH shared secret (e.g., for secondary encrypted channels). - /// - /// Returns `None` if the shared secret hasn't been computed yet. - fn shared_secret_bytes(&self) -> Option<&[u8]>; - - fn compute_exchange_hash( - &self, - key: &[u8], - exchange: &Exchange, - buffer: &mut CryptoVec, - ) -> Result, Error>; - - fn compute_keys( - &self, - session_id: &[u8], - exchange_hash: &[u8], - cipher: cipher::Name, - remote_to_local_mac: mac::Name, - local_to_remote_mac: mac::Name, - is_server: bool, - ) -> Result; -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub struct Name(&'static str); -impl AsRef for Name { - fn as_ref(&self) -> &str { - self.0 - } -} - -impl Encode for Name { - delegate! { to self.as_ref() { - fn encoded_len(&self) -> Result; - fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error>; - }} -} - -impl TryFrom<&str> for Name { - type Error = (); - fn try_from(s: &str) -> Result { - KEXES.keys().find(|x| x.0 == s).map(|x| **x).ok_or(()) - } -} - -/// `curve25519-sha256` -pub const CURVE25519: Name = Name("curve25519-sha256"); -/// `curve25519-sha256@libssh.org` -pub const CURVE25519_PRE_RFC_8731: Name = Name("curve25519-sha256@libssh.org"); -/// `mlkem768x25519-sha256` -pub const MLKEM768X25519_SHA256: Name = Name("mlkem768x25519-sha256"); -/// `diffie-hellman-group-exchange-sha1`. -pub const DH_GEX_SHA1: Name = Name("diffie-hellman-group-exchange-sha1"); -/// `diffie-hellman-group-exchange-sha256`. -pub const DH_GEX_SHA256: Name = Name("diffie-hellman-group-exchange-sha256"); -/// `diffie-hellman-group1-sha1` -pub const DH_G1_SHA1: Name = Name("diffie-hellman-group1-sha1"); -/// `diffie-hellman-group14-sha1` -pub const DH_G14_SHA1: Name = Name("diffie-hellman-group14-sha1"); -/// `diffie-hellman-group14-sha256` -pub const DH_G14_SHA256: Name = Name("diffie-hellman-group14-sha256"); -/// `diffie-hellman-group15-sha512` -pub const DH_G15_SHA512: Name = Name("diffie-hellman-group15-sha512"); -/// `diffie-hellman-group16-sha512` -pub const DH_G16_SHA512: Name = Name("diffie-hellman-group16-sha512"); -/// `diffie-hellman-group17-sha512` -pub const DH_G17_SHA512: Name = Name("diffie-hellman-group17-sha512"); -/// `diffie-hellman-group18-sha512` -pub const DH_G18_SHA512: Name = Name("diffie-hellman-group18-sha512"); -/// `ecdh-sha2-nistp256` -pub const ECDH_SHA2_NISTP256: Name = Name("ecdh-sha2-nistp256"); -/// `ecdh-sha2-nistp384` -pub const ECDH_SHA2_NISTP384: Name = Name("ecdh-sha2-nistp384"); -/// `ecdh-sha2-nistp521` -pub const ECDH_SHA2_NISTP521: Name = Name("ecdh-sha2-nistp521"); -/// `none` -pub const NONE: Name = Name("none"); -/// `ext-info-c` -pub const EXTENSION_SUPPORT_AS_CLIENT: Name = Name("ext-info-c"); -/// `ext-info-s` -pub const EXTENSION_SUPPORT_AS_SERVER: Name = Name("ext-info-s"); -/// `kex-strict-c-v00@openssh.com` -pub const EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT: Name = Name("kex-strict-c-v00@openssh.com"); -/// `kex-strict-s-v00@openssh.com` -pub const EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER: Name = Name("kex-strict-s-v00@openssh.com"); - -const _CURVE25519: Curve25519KexType = Curve25519KexType {}; -const _DH_GEX_SHA1: DhGexSha1KexType = DhGexSha1KexType {}; -const _DH_GEX_SHA256: DhGexSha256KexType = DhGexSha256KexType {}; -const _DH_G1_SHA1: DhGroup1Sha1KexType = DhGroup1Sha1KexType {}; -const _DH_G14_SHA1: DhGroup14Sha1KexType = DhGroup14Sha1KexType {}; -const _DH_G14_SHA256: DhGroup14Sha256KexType = DhGroup14Sha256KexType {}; -const _DH_G15_SHA512: DhGroup15Sha512KexType = DhGroup15Sha512KexType {}; -const _DH_G16_SHA512: DhGroup16Sha512KexType = DhGroup16Sha512KexType {}; -const _DH_G17_SHA512: DhGroup17Sha512KexType = DhGroup17Sha512KexType {}; -const _DH_G18_SHA512: DhGroup18Sha512KexType = DhGroup18Sha512KexType {}; -const _ECDH_SHA2_NISTP256: EcdhNistP256KexType = EcdhNistP256KexType {}; -const _ECDH_SHA2_NISTP384: EcdhNistP384KexType = EcdhNistP384KexType {}; -const _ECDH_SHA2_NISTP521: EcdhNistP521KexType = EcdhNistP521KexType {}; -const _MLKEM768X25519_SHA256: MlKem768X25519KexType = MlKem768X25519KexType {}; -const _NONE: none::NoneKexType = none::NoneKexType {}; - -pub const ALL_KEX_ALGORITHMS: &[&Name] = &[ - &MLKEM768X25519_SHA256, - &CURVE25519, - &CURVE25519_PRE_RFC_8731, - &DH_GEX_SHA1, - &DH_GEX_SHA256, - &DH_G1_SHA1, - &DH_G14_SHA1, - &DH_G14_SHA256, - &DH_G15_SHA512, - &DH_G16_SHA512, - &DH_G17_SHA512, - &DH_G18_SHA512, - &ECDH_SHA2_NISTP256, - &ECDH_SHA2_NISTP384, - &ECDH_SHA2_NISTP521, - &NONE, -]; - -pub(crate) static KEXES: LazyLock> = - LazyLock::new(|| { - let mut h: HashMap<&'static Name, &(dyn KexType + Send + Sync)> = HashMap::new(); - h.insert(&MLKEM768X25519_SHA256, &_MLKEM768X25519_SHA256); - h.insert(&CURVE25519, &_CURVE25519); - h.insert(&CURVE25519_PRE_RFC_8731, &_CURVE25519); - h.insert(&DH_GEX_SHA1, &_DH_GEX_SHA1); - h.insert(&DH_GEX_SHA256, &_DH_GEX_SHA256); - h.insert(&DH_G18_SHA512, &_DH_G18_SHA512); - h.insert(&DH_G17_SHA512, &_DH_G17_SHA512); - h.insert(&DH_G16_SHA512, &_DH_G16_SHA512); - h.insert(&DH_G15_SHA512, &_DH_G15_SHA512); - h.insert(&DH_G14_SHA256, &_DH_G14_SHA256); - h.insert(&DH_G14_SHA1, &_DH_G14_SHA1); - h.insert(&DH_G1_SHA1, &_DH_G1_SHA1); - h.insert(&ECDH_SHA2_NISTP256, &_ECDH_SHA2_NISTP256); - h.insert(&ECDH_SHA2_NISTP384, &_ECDH_SHA2_NISTP384); - h.insert(&ECDH_SHA2_NISTP521, &_ECDH_SHA2_NISTP521); - h.insert(&NONE, &_NONE); - assert_eq!(ALL_KEX_ALGORITHMS.len(), h.len()); - h - }); - -thread_local! { - static KEY_BUF: RefCell = RefCell::new(CryptoVec::new()); - static NONCE_BUF: RefCell = RefCell::new(CryptoVec::new()); - static MAC_BUF: RefCell = RefCell::new(CryptoVec::new()); - static BUFFER: RefCell = RefCell::new(CryptoVec::new()); -} - -pub(crate) enum SharedSecret { - Mpint(CryptoVec), - String(CryptoVec), -} - -impl SharedSecret { - pub fn from_mpint(bytes: &[u8]) -> Result { - let mut encoded = CryptoVec::new(); - encode_mpint(bytes, &mut encoded)?; - Ok(SharedSecret::Mpint(encoded)) - } - - pub fn from_string(bytes: &[u8]) -> Result { - let mut encoded = CryptoVec::new(); - bytes.encode(&mut encoded)?; - Ok(SharedSecret::String(encoded)) - } - - pub fn as_bytes(&self) -> &[u8] { - match self { - SharedSecret::Mpint(v) | SharedSecret::String(v) => v.as_ref(), - } - } -} - -pub(crate) fn compute_keys( - shared_secret: Option<&SharedSecret>, - session_id: &[u8], - exchange_hash: &[u8], - cipher: cipher::Name, - remote_to_local_mac: mac::Name, - local_to_remote_mac: mac::Name, - is_server: bool, -) -> Result { - let cipher = CIPHERS.get(&cipher).ok_or(Error::UnknownAlgo)?; - let remote_to_local_mac = MACS.get(&remote_to_local_mac).ok_or(Error::UnknownAlgo)?; - let local_to_remote_mac = MACS.get(&local_to_remote_mac).ok_or(Error::UnknownAlgo)?; - - // https://tools.ietf.org/html/rfc4253#section-7.2 - BUFFER.with(|buffer| { - KEY_BUF.with(|key| { - NONCE_BUF.with(|nonce| { - MAC_BUF.with(|mac| { - let compute_key = |c, key: &mut CryptoVec, len| -> Result<(), Error> { - let mut buffer = buffer.borrow_mut(); - buffer.clear(); - key.clear(); - - if let Some(shared) = shared_secret { - buffer.extend(shared.as_bytes()); - } - - buffer.extend(exchange_hash.as_ref()); - buffer.push(c); - buffer.extend(session_id.as_ref()); - let hash = { - let mut hasher = D::new(); - hasher.update(&buffer[..]); - hasher.finalize() - }; - key.extend(hash.as_ref()); - - while key.len() < len { - // extend. - buffer.clear(); - if let Some(shared) = shared_secret { - buffer.extend(shared.as_bytes()); - } - buffer.extend(exchange_hash.as_ref()); - buffer.extend(key); - let hash = { - let mut hasher = D::new(); - hasher.update(&buffer[..]); - hasher.finalize() - }; - key.extend(hash.as_ref()); - } - - key.resize(len); - Ok(()) - }; - - let (local_to_remote, remote_to_local) = if is_server { - (b'D', b'C') - } else { - (b'C', b'D') - }; - - let (local_to_remote_nonce, remote_to_local_nonce) = if is_server { - (b'B', b'A') - } else { - (b'A', b'B') - }; - - let (local_to_remote_mac_key, remote_to_local_mac_key) = if is_server { - (b'F', b'E') - } else { - (b'E', b'F') - }; - - let mut key = key.borrow_mut(); - let mut nonce = nonce.borrow_mut(); - let mut mac = mac.borrow_mut(); - - compute_key(local_to_remote, &mut key, cipher.key_len())?; - compute_key(local_to_remote_nonce, &mut nonce, cipher.nonce_len())?; - compute_key( - local_to_remote_mac_key, - &mut mac, - local_to_remote_mac.key_len(), - )?; - - let local_to_remote = - cipher.make_sealing_key(&key, &nonce, &mac, *local_to_remote_mac); - - compute_key(remote_to_local, &mut key, cipher.key_len())?; - compute_key(remote_to_local_nonce, &mut nonce, cipher.nonce_len())?; - compute_key( - remote_to_local_mac_key, - &mut mac, - remote_to_local_mac.key_len(), - )?; - let remote_to_local = - cipher.make_opening_key(&key, &nonce, &mac, *remote_to_local_mac); - - Ok(super::cipher::CipherPair { - local_to_remote, - remote_to_local, - }) - }) - }) - }) - }) -} - -// NOTE: using MpInt::from_bytes().encode() will randomly fail, -// I'm assuming it's due to specific byte values / padding but no time to investigate -#[allow(clippy::indexing_slicing)] // length is known -pub(crate) fn encode_mpint(s: &[u8], w: &mut W) -> Result<(), Error> { - // Skip initial 0s. - let mut i = 0; - while i < s.len() && s[i] == 0 { - i += 1 - } - // If the first non-zero is >= 128, write its length (u32, BE), followed by 0. - if s[i] & 0x80 != 0 { - ((s.len() - i + 1) as u32).encode(w)?; - 0u8.encode(w)?; - } else { - ((s.len() - i) as u32).encode(w)?; - } - w.write(&s[i..])?; - Ok(()) -} diff --git a/crates/bssh-russh/src/kex/none.rs b/crates/bssh-russh/src/kex/none.rs deleted file mode 100644 index fcf967a6..00000000 --- a/crates/bssh-russh/src/kex/none.rs +++ /dev/null @@ -1,73 +0,0 @@ -use ssh_encoding::Writer; - -use super::{KexAlgorithm, KexAlgorithmImplementor, KexType}; - -pub struct NoneKexType {} - -impl KexType for NoneKexType { - fn make(&self) -> KexAlgorithm { - NoneKexAlgorithm {}.into() - } -} - -#[doc(hidden)] -pub struct NoneKexAlgorithm {} - -impl KexAlgorithmImplementor for NoneKexAlgorithm { - fn skip_exchange(&self) -> bool { - true - } - - fn server_dh( - &mut self, - _exchange: &mut crate::session::Exchange, - _payload: &[u8], - ) -> Result<(), crate::Error> { - Ok(()) - } - - fn client_dh( - &mut self, - _client_ephemeral: &mut Vec, - _buf: &mut impl Writer, - ) -> Result<(), crate::Error> { - Ok(()) - } - - fn compute_shared_secret(&mut self, _remote_pubkey: &[u8]) -> Result<(), crate::Error> { - Ok(()) - } - - fn shared_secret_bytes(&self) -> Option<&[u8]> { - None - } - - fn compute_exchange_hash( - &self, - _key: &[u8], - _exchange: &crate::session::Exchange, - _buffer: &mut russh_cryptovec::CryptoVec, - ) -> Result, crate::Error> { - Ok(Vec::new()) - } - - fn compute_keys( - &self, - session_id: &[u8], - exchange_hash: &[u8], - cipher: crate::cipher::Name, - remote_to_local_mac: crate::mac::Name, - local_to_remote_mac: crate::mac::Name, - is_server: bool, - ) -> Result { - super::compute_keys::( - None, - session_id, - exchange_hash, - cipher, - remote_to_local_mac, - local_to_remote_mac, - is_server, - ) - } -} diff --git a/crates/bssh-russh/src/keys/agent/client.rs b/crates/bssh-russh/src/keys/agent/client.rs deleted file mode 100644 index b2a485e5..00000000 --- a/crates/bssh-russh/src/keys/agent/client.rs +++ /dev/null @@ -1,624 +0,0 @@ -use core::str; - -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use log::{debug, error}; -use ssh_encoding::{Decode, Encode, Reader}; -use ssh_key::{Algorithm, Certificate, HashAlg, PrivateKey, PublicKey, Signature}; -use tokio; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; - -use super::{AgentIdentity, Constraint, msg}; -use crate::CryptoVec; -use crate::helpers::EncodedExt; -use crate::keys::{Error, key}; - -pub trait AgentStream: AsyncRead + AsyncWrite {} - -impl AgentStream for S {} - -const MAX_AGENT_FRAME_LEN: usize = 256 * 1024; - -/// SSH agent client. -pub struct AgentClient { - stream: S, - buf: Vec, -} - -impl AgentClient { - /// Wraps the internal stream in a Box, allowing different client - /// implementations to have the same type - pub fn dynamic(self) -> AgentClient> { - AgentClient { - stream: Box::new(self.stream), - buf: self.buf, - } - } - - pub fn into_inner(self) -> Box { - Box::new(self.stream) - } -} - -// https://tools.ietf.org/html/draft-miller-ssh-agent-00#section-4.1 -impl AgentClient { - /// Build a future that connects to an SSH agent via the provided - /// stream (on Unix, usually a Unix-domain socket). - pub fn connect(stream: S) -> Self { - AgentClient { - stream, - buf: Vec::new(), - } - } -} - -#[cfg(unix)] -impl AgentClient { - /// Connect to an SSH agent via the provided - /// stream (on Unix, usually a Unix-domain socket). - pub async fn connect_uds>(path: P) -> Result { - let stream = tokio::net::UnixStream::connect(path).await?; - Ok(AgentClient { - stream, - buf: Vec::new(), - }) - } - - /// Connect to an SSH agent specified by the SSH_AUTH_SOCK - /// environment variable. - pub async fn connect_env() -> Result { - let var = if let Ok(var) = std::env::var("SSH_AUTH_SOCK") { - var - } else { - return Err(Error::EnvVar("SSH_AUTH_SOCK")); - }; - match Self::connect_uds(var).await { - Err(Error::IO(io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => { - Err(Error::BadAuthSock) - } - owise => owise, - } - } -} - -#[cfg(windows)] -const ERROR_PIPE_BUSY: u32 = 231u32; - -#[cfg(windows)] -impl AgentClient { - /// Connect to a running Pageant instance - pub async fn connect_pageant() -> Result { - Ok(Self::connect(pageant::PageantStream::new().await?)) - } -} - -#[cfg(windows)] -impl AgentClient { - /// Connect to an SSH agent via a Windows named pipe - pub async fn connect_named_pipe>(path: P) -> Result { - let stream = loop { - match tokio::net::windows::named_pipe::ClientOptions::new().open(path.as_ref()) { - Ok(client) => break client, - Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (), - Err(e) => return Err(e.into()), - } - - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - }; - - Ok(AgentClient { - stream, - buf: Vec::new(), - }) - } -} - -impl AgentClient { - async fn read_frame(&mut self) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - self.stream.read_exact(&mut self.buf).await?; - - let len = BigEndian::read_u32(&self.buf) as usize; - if len > MAX_AGENT_FRAME_LEN { - return Err(Error::AgentProtocolError); - } - - self.buf.clear(); - self.buf.resize(len, 0); - self.stream.read_exact(&mut self.buf).await?; - Ok(()) - } - - async fn read_response(&mut self) -> Result<(), Error> { - // Writing the message - self.stream.write_all(&self.buf).await?; - self.stream.flush().await?; - self.read_frame().await - } - - async fn read_success(&mut self) -> Result<(), Error> { - self.read_response().await?; - if self.buf.first() == Some(&msg::SUCCESS) { - Ok(()) - } else { - Err(Error::AgentFailure) - } - } - - /// Send a key to the agent, with a (possibly empty) slice of - /// constraints to apply when using the key to sign. - pub async fn add_identity( - &mut self, - key: &PrivateKey, - constraints: &[Constraint], - ) -> Result<(), Error> { - // See IETF draft-miller-ssh-agent-13, section 3.2 for format. - // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent - self.buf.clear(); - self.buf.resize(4, 0); - if constraints.is_empty() { - self.buf.push(msg::ADD_IDENTITY) - } else { - self.buf.push(msg::ADD_ID_CONSTRAINED) - } - - key.key_data().encode(&mut self.buf)?; - "".encode(&mut self.buf)?; // comment field - - if !constraints.is_empty() { - for cons in constraints { - match *cons { - Constraint::KeyLifetime { seconds } => { - msg::CONSTRAIN_LIFETIME.encode(&mut self.buf)?; - seconds.encode(&mut self.buf)?; - } - Constraint::Confirm => self.buf.push(msg::CONSTRAIN_CONFIRM), - Constraint::Extensions { - ref name, - ref details, - } => { - msg::CONSTRAIN_EXTENSION.encode(&mut self.buf)?; - name.encode(&mut self.buf)?; - details.encode(&mut self.buf)?; - } - } - } - } - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - - self.read_success().await?; - Ok(()) - } - - /// Add a smart card to the agent, with a (possibly empty) set of - /// constraints to apply when signing. - pub async fn add_smartcard_key( - &mut self, - id: &str, - pin: &[u8], - constraints: &[Constraint], - ) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - if constraints.is_empty() { - self.buf.push(msg::ADD_SMARTCARD_KEY) - } else { - self.buf.push(msg::ADD_SMARTCARD_KEY_CONSTRAINED) - } - id.encode(&mut self.buf)?; - pin.encode(&mut self.buf)?; - if !constraints.is_empty() { - (constraints.len() as u32).encode(&mut self.buf)?; - for cons in constraints { - match *cons { - Constraint::KeyLifetime { seconds } => { - msg::CONSTRAIN_LIFETIME.encode(&mut self.buf)?; - seconds.encode(&mut self.buf)?; - } - Constraint::Confirm => self.buf.push(msg::CONSTRAIN_CONFIRM), - Constraint::Extensions { - ref name, - ref details, - } => { - msg::CONSTRAIN_EXTENSION.encode(&mut self.buf)?; - name.encode(&mut self.buf)?; - details.encode(&mut self.buf)?; - } - } - } - } - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - self.read_response().await?; - Ok(()) - } - - /// Lock the agent, making it refuse to sign until unlocked. - pub async fn lock(&mut self, passphrase: &[u8]) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - self.buf.push(msg::LOCK); - passphrase.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - self.read_response().await?; - Ok(()) - } - - /// Unlock the agent, allowing it to sign again. - pub async fn unlock(&mut self, passphrase: &[u8]) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - msg::UNLOCK.encode(&mut self.buf)?; - passphrase.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - #[allow(clippy::indexing_slicing)] // static length - BigEndian::write_u32(&mut self.buf[..], len as u32); - self.read_response().await?; - Ok(()) - } - - /// Ask the agent for a list of identities, including certificates. - pub async fn request_identities(&mut self) -> Result, Error> { - self.buf.clear(); - self.buf.resize(4, 0); - msg::REQUEST_IDENTITIES.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - - self.read_response().await?; - debug!("identities: {:?}", &self.buf[..]); - let mut identities = Vec::new(); - - #[allow(clippy::indexing_slicing)] // static length - if let Some((&msg::IDENTITIES_ANSWER, mut r)) = self.buf.split_first() { - let n = u32::decode(&mut r)?; - for _ in 0..n { - let key_blob = Bytes::decode(&mut r)?; - let comment = String::decode(&mut r)?; - - // Check if blob starts with a certificate algorithm by reading the algorithm string. - // Certificate algorithms end with "-cert-v01@openssh.com". - // This avoids parsing the blob twice for regular keys. - let identity = if Self::is_certificate_blob(&key_blob) { - match Certificate::decode(&mut key_blob.as_ref()) { - Ok(cert) => AgentIdentity::Certificate { certificate: cert, comment }, - Err(_) => { - // Fallback to public key if certificate parsing fails - let key = key::parse_public_key(&key_blob)?; - AgentIdentity::PublicKey { key, comment } - } - } - } else { - let key = key::parse_public_key(&key_blob)?; - AgentIdentity::PublicKey { key, comment } - }; - identities.push(identity); - } - } - - Ok(identities) - } - - /// Check if a key blob appears to be a certificate by examining the algorithm prefix. - /// Certificate algorithms end with "-cert-v01@openssh.com". - fn is_certificate_blob(blob: &[u8]) -> bool { - // The blob starts with a length-prefixed string containing the algorithm name. - // Read the length (4 bytes, big-endian) and then the algorithm string. - let Some(len_bytes) = blob.get(..4) else { - return false; - }; - let alg_len = BigEndian::read_u32(len_bytes) as usize; - let Some(alg_bytes) = blob.get(4..4 + alg_len) else { - return false; - }; - if let Ok(alg_str) = str::from_utf8(alg_bytes) { - alg_str.ends_with("-cert-v01@openssh.com") - } else { - false - } - } - - /// Ask the agent to sign the supplied piece of data. - pub async fn sign_request( - &mut self, - identity: &AgentIdentity, - hash_alg: Option, - data: Vec, - ) -> Result, Error> { - match identity { - AgentIdentity::PublicKey { key, .. } => self.sign_request_pk(key, hash_alg, data).await, - AgentIdentity::Certificate { certificate, .. } => { - self.sign_request_cert(certificate, hash_alg, data).await - } - } - } - - async fn sign_request_pk( - &mut self, - public: &PublicKey, - hash_alg: Option, - mut data: Vec, - ) -> Result, Error> { - debug!("sign_request: {data:?}"); - let hash = self.prepare_sign_request(public, hash_alg, &data)?; - - self.read_response().await?; - - match self.buf.split_first() { - Some((&msg::SIGN_RESPONSE, mut r)) => { - self.write_signature(&mut r, hash, &mut data)?; - Ok(data) - } - Some((&msg::FAILURE, _)) => Err(Error::AgentFailure), - _ => { - debug!("self.buf = {:?}", &self.buf[..]); - Err(Error::AgentProtocolError) - } - } - } - - /// Ask the agent to sign data using a certificate identity. - /// - /// This sends the certificate blob to the agent (not just the public key), - /// allowing the agent to match it to the correct private key. - /// - /// For RSA certificates, you can specify the hash algorithm to use. - async fn sign_request_cert( - &mut self, - cert: &Certificate, - hash_alg: Option, - mut data: Vec, - ) -> Result, Error> { - debug!("sign_request_cert: {data:?}"); - - self.buf.clear(); - self.buf.resize(4, 0); - msg::SIGN_REQUEST.encode(&mut self.buf)?; - cert.to_bytes()?.encode(&mut self.buf)?; - data.encode(&mut self.buf)?; - - // Calculate hash flag for RSA certificates (same logic as prepare_sign_request) - let hash = match cert.algorithm() { - Algorithm::Rsa { .. } => match hash_alg { - Some(HashAlg::Sha256) => 2, - Some(HashAlg::Sha512) => 4, - _ => 0, - }, - _ => 0, - }; - - hash.encode(&mut self.buf)?; - - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - - self.read_response().await?; - - match self.buf.split_first() { - Some((&msg::SIGN_RESPONSE, mut r)) => { - self.write_signature(&mut r, hash, &mut data)?; - Ok(data) - } - Some((&msg::FAILURE, _)) => Err(Error::AgentFailure), - _ => { - debug!("self.buf = {:?}", &self.buf[..]); - Err(Error::AgentProtocolError) - } - } - } - - fn prepare_sign_request( - &mut self, - public: &ssh_key::PublicKey, - hash_alg: Option, - data: &[u8], - ) -> Result { - self.buf.clear(); - self.buf.resize(4, 0); - msg::SIGN_REQUEST.encode(&mut self.buf)?; - public.key_data().encoded()?.encode(&mut self.buf)?; - data.encode(&mut self.buf)?; - debug!("public = {public:?}"); - - let hash = match public.algorithm() { - Algorithm::Rsa { .. } => match hash_alg { - Some(HashAlg::Sha256) => 2, - Some(HashAlg::Sha512) => 4, - _ => 0, - }, - _ => 0, - }; - - hash.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - Ok(hash) - } - - fn write_signature( - &self, - r: &mut R, - hash: u32, - data: &mut Vec, - ) -> Result<(), Error> { - let mut resp = &Bytes::decode(r)?[..]; - let t = String::decode(&mut resp)?; - if (hash == 2 && t == "rsa-sha2-256") || (hash == 4 && t == "rsa-sha2-512") || hash == 0 { - let sig = Bytes::decode(&mut resp)?; - let is_sk_signature = t.starts_with("sk-"); - (t.len() + sig.len() + 8 + if is_sk_signature { 5 } else { 0 }).encode(data)?; - t.encode(data)?; - sig.encode(data)?; - if is_sk_signature { - let flags = u8::decode(&mut resp)?; - let counter = u32::decode(&mut resp)?; - flags.encode(data)?; - counter.encode(data)?; - } - Ok(()) - } else { - error!("unexpected agent signature type: {t:?}"); - Err(Error::AgentProtocolError) - } - } - - /// Ask the agent to sign the supplied piece of data. - pub fn sign_request_base64( - mut self, - public: &ssh_key::PublicKey, - hash_alg: Option, - data: &[u8], - ) -> impl futures::Future)> { - debug!("sign_request: {data:?}"); - let r = self.prepare_sign_request(public, hash_alg, data); - async move { - if let Err(e) = r { - return (self, Err(e)); - } - - let resp = self.read_response().await; - if let Err(e) = resp { - return (self, Err(e)); - } - - #[allow(clippy::indexing_slicing)] // length is checked - if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { - let base64 = data_encoding::BASE64_NOPAD.encode(&self.buf[1..]); - (self, Ok(base64)) - } else { - (self, Ok(String::new())) - } - } - } - - /// Ask the agent to sign the supplied piece of data, and return a `Signature`. - pub async fn sign_request_signature( - &mut self, - public: &ssh_key::PublicKey, - hash_alg: Option, - data: &[u8], - ) -> Result { - debug!("sign_request: {data:?}"); - - self.prepare_sign_request(public, hash_alg, data)?; - self.read_response().await?; - - match self.buf.split_first() { - Some((&msg::SIGN_RESPONSE, mut r)) => { - let mut resp = &Bytes::decode(&mut r)?[..]; - let sig = Signature::decode(&mut resp)?; - Ok(sig) - } - _ => Err(Error::AgentProtocolError), - } - } - - /// Ask the agent to remove a key from its memory. - pub async fn remove_identity(&mut self, public: &ssh_key::PublicKey) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - self.buf.push(msg::REMOVE_IDENTITY); - public.key_data().encoded()?.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - self.read_response().await?; - Ok(()) - } - - /// Ask the agent to remove a smartcard from its memory. - pub async fn remove_smartcard_key(&mut self, id: &str, pin: &[u8]) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - msg::REMOVE_SMARTCARD_KEY.encode(&mut self.buf)?; - id.encode(&mut self.buf)?; - pin.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - BigEndian::write_u32(&mut self.buf[..], len as u32); - self.read_response().await?; - Ok(()) - } - - /// Ask the agent to forget all known keys. - pub async fn remove_all_identities(&mut self) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - msg::REMOVE_ALL_IDENTITIES.encode(&mut self.buf)?; - 1u32.encode(&mut self.buf)?; - self.read_success().await?; - Ok(()) - } - - /// Send a custom message to the agent. - pub async fn extension(&mut self, typ: &[u8], ext: &[u8]) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - msg::EXTENSION.encode(&mut self.buf)?; - typ.encode(&mut self.buf)?; - ext.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - (len as u32).encode(&mut self.buf)?; - self.read_response().await?; - Ok(()) - } - - /// Ask the agent what extensions about supported extensions. - pub async fn query_extension(&mut self, typ: &[u8], mut ext: CryptoVec) -> Result { - self.buf.clear(); - self.buf.resize(4, 0); - msg::EXTENSION.encode(&mut self.buf)?; - typ.encode(&mut self.buf)?; - let len = self.buf.len() - 4; - (len as u32).encode(&mut self.buf)?; - self.read_response().await?; - - match self.buf.split_first() { - Some((&msg::SUCCESS, mut r)) => { - ext.extend(&Bytes::decode(&mut r)?); - Ok(true) - } - _ => Ok(false), - } - } -} - -#[cfg(test)] -mod tests { - use byteorder::{BigEndian, ByteOrder}; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - use super::{AgentClient, MAX_AGENT_FRAME_LEN}; - use crate::keys::Error; - - #[test] - fn oversized_agent_response_is_rejected_before_allocation() -> std::io::Result<()> { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - runtime.block_on(async { - let (mut writer, reader) = tokio::io::duplex(64); - let server = tokio::spawn(async move { - let mut frame = [0u8; 4]; - writer.read_exact(&mut frame).await?; - let len = BigEndian::read_u32(&frame) as usize; - let mut body = vec![0; len]; - writer.read_exact(&mut body).await?; - - BigEndian::write_u32(&mut frame, (MAX_AGENT_FRAME_LEN + 1) as u32); - writer.write_all(&frame).await?; - Ok::<(), std::io::Error>(()) - }); - - let mut client = AgentClient::connect(reader); - let err = client.request_identities().await.unwrap_err(); - assert!(matches!(err, Error::AgentProtocolError)); - server.await.expect("server task")?; - Ok::<(), std::io::Error>(()) - })?; - - Ok(()) - } -} diff --git a/crates/bssh-russh/src/keys/agent/mod.rs b/crates/bssh-russh/src/keys/agent/mod.rs deleted file mode 100644 index 433535da..00000000 --- a/crates/bssh-russh/src/keys/agent/mod.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::borrow::Cow; - -use ssh_key::{Certificate, PublicKey}; - -/// Write clients for SSH agents. -pub mod client; -mod msg; -/// Write servers for SSH agents. -pub mod server; - -/// Constraints on how keys can be used -#[derive(Debug, PartialEq, Eq)] -pub enum Constraint { - /// The key shall disappear from the agent's memory after that many seconds. - KeyLifetime { seconds: u32 }, - /// Signatures need to be confirmed by the agent (for instance using a dialog). - Confirm, - /// Custom constraints - Extensions { name: Vec, details: Vec }, -} - -/// An identity held by an SSH agent, which may be either a plain public key -/// or an OpenSSH certificate. -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -pub enum AgentIdentity { - /// A plain public key - PublicKey { - /// The public key - key: PublicKey, - /// Comment associated with this identity - comment: String, - }, - /// An OpenSSH certificate - Certificate { - /// The certificate (contains public key plus CA signature, principals, validity, etc.) - certificate: Certificate, - /// Comment associated with this identity - comment: String, - }, -} - -impl From for AgentIdentity { - fn from(key: PublicKey) -> Self { - Self::PublicKey { - key, - comment: String::new(), - } - } -} - -impl From for AgentIdentity { - fn from(certificate: Certificate) -> Self { - Self::Certificate { - certificate, - comment: String::new(), - } - } -} - -impl AgentIdentity { - /// Returns the underlying public key. - /// For certificates, extracts the public key from the certificate. - /// Returns a borrowed reference for plain keys, or an owned value for certificates. - pub fn public_key(&self) -> Cow<'_, PublicKey> { - match self { - Self::PublicKey { key, .. } => Cow::Borrowed(key), - Self::Certificate { certificate, .. } => { - Cow::Owned(PublicKey::new(certificate.public_key().clone(), "")) - } - } - } - - /// Returns the comment associated with this identity. - pub fn comment(&self) -> &str { - match self { - Self::PublicKey { comment, .. } => comment, - Self::Certificate { comment, .. } => comment, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ssh_key::{PrivateKey, certificate}; - - fn create_test_certificate() -> Certificate { - use std::time::{SystemTime, UNIX_EPOCH}; - - // Create a CA key - let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Create a user key to be certified - let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Build and sign the certificate with reasonable validity window - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let valid_after = now - 3600; // 1 hour ago - let valid_before = now + 86400 * 365; // 1 year from now - - let mut builder = certificate::Builder::new_with_random_nonce( - &mut rand::rng(), - user_key.public_key(), - valid_after, - valid_before, - ) - .unwrap(); - - builder.serial(1).unwrap(); - builder.key_id("test-cert").unwrap(); - builder.cert_type(certificate::CertType::User).unwrap(); - builder.valid_principal("testuser").unwrap(); - builder.sign(&ca_key).unwrap() - } - - #[test] - fn test_agent_identity_public_key_variant() { - let key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519) - .unwrap() - .public_key() - .clone(); - let comment = "test-key-comment".to_string(); - - let identity = AgentIdentity::PublicKey { - key: key.clone(), - comment: comment.clone(), - }; - - // Test public_key() returns borrowed reference - let retrieved_key = identity.public_key(); - assert!(matches!(retrieved_key, Cow::Borrowed(_))); - assert_eq!(retrieved_key.key_data(), key.key_data()); - - // Test comment() - assert_eq!(identity.comment(), "test-key-comment"); - } - - #[test] - fn test_agent_identity_certificate_variant() { - let cert = create_test_certificate(); - let comment = "test-cert-comment".to_string(); - - let identity = AgentIdentity::Certificate { - certificate: cert.clone(), - comment: comment.clone(), - }; - - // Test public_key() returns owned value extracted from cert - let retrieved_key = identity.public_key(); - assert!(matches!(retrieved_key, Cow::Owned(_))); - assert_eq!(retrieved_key.key_data(), cert.public_key()); - - // Test comment() - assert_eq!(identity.comment(), "test-cert-comment"); - } - - #[test] - fn test_agent_identity_clone() { - let key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519) - .unwrap() - .public_key() - .clone(); - - let identity = AgentIdentity::PublicKey { - key, - comment: "cloneable".to_string(), - }; - - let cloned = identity.clone(); - assert_eq!(cloned.comment(), identity.comment()); - } - - #[test] - fn test_agent_identity_debug() { - let key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519) - .unwrap() - .public_key() - .clone(); - - let identity = AgentIdentity::PublicKey { - key, - comment: "debug-test".to_string(), - }; - - // Just verify Debug is implemented and doesn't panic - let debug_str = format!("{:?}", identity); - assert!(debug_str.contains("PublicKey")); - } -} diff --git a/crates/bssh-russh/src/keys/agent/msg.rs b/crates/bssh-russh/src/keys/agent/msg.rs deleted file mode 100644 index d732e674..00000000 --- a/crates/bssh-russh/src/keys/agent/msg.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub const FAILURE: u8 = 5; -pub const SUCCESS: u8 = 6; -pub const IDENTITIES_ANSWER: u8 = 12; -pub const SIGN_RESPONSE: u8 = 14; -// pub const EXTENSION_FAILURE: u8 = 28; - -pub const REQUEST_IDENTITIES: u8 = 11; -pub const SIGN_REQUEST: u8 = 13; -pub const ADD_IDENTITY: u8 = 17; -pub const REMOVE_IDENTITY: u8 = 18; -pub const REMOVE_ALL_IDENTITIES: u8 = 19; -pub const ADD_ID_CONSTRAINED: u8 = 25; -pub const ADD_SMARTCARD_KEY: u8 = 20; -pub const REMOVE_SMARTCARD_KEY: u8 = 21; -pub const LOCK: u8 = 22; -pub const UNLOCK: u8 = 23; -pub const ADD_SMARTCARD_KEY_CONSTRAINED: u8 = 26; -pub const EXTENSION: u8 = 27; - -pub const CONSTRAIN_LIFETIME: u8 = 1; -pub const CONSTRAIN_CONFIRM: u8 = 2; -// pub const CONSTRAIN_MAXSIGN: u8 = 3; -pub const CONSTRAIN_EXTENSION: u8 = 255; diff --git a/crates/bssh-russh/src/keys/agent/server.rs b/crates/bssh-russh/src/keys/agent/server.rs deleted file mode 100644 index 48a4149b..00000000 --- a/crates/bssh-russh/src/keys/agent/server.rs +++ /dev/null @@ -1,403 +0,0 @@ -use std::collections::HashMap; -use std::marker::Sync; -use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime}; - -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use futures::future::Future; -use futures::stream::{Stream, StreamExt}; -use ssh_encoding::{Decode, Encode, Reader}; -use ssh_key::PrivateKey; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio::time::sleep; -use {std, tokio}; - -use super::{msg, Constraint}; -use crate::helpers::{sign_with_hash_alg, EncodedExt}; -use crate::keys::key::PrivateKeyWithHashAlg; -use crate::keys::Error; -use crate::CryptoVec; - -const MAX_AGENT_FRAME_LEN: usize = 256 * 1024; - -#[derive(Clone)] -#[allow(clippy::type_complexity)] -struct KeyStore(Arc, (Arc, SystemTime, Vec)>>>); - -#[derive(Clone)] -struct Lock(Arc>); - -#[allow(missing_docs)] -#[derive(Debug)] -pub enum ServerError { - E(E), - Error(Error), -} - -pub enum MessageType { - RequestKeys, - AddKeys, - RemoveKeys, - RemoveAllKeys, - Sign, - Lock, - Unlock, -} - -#[cfg_attr(feature = "async-trait", async_trait::async_trait)] -pub trait Agent: Clone + Send + 'static { - fn confirm( - self, - _pk: Arc, - ) -> Box + Unpin + Send> { - Box::new(futures::future::ready((self, true))) - } - - fn confirm_request(&self, _msg: MessageType) -> impl Future + Send { - async { true } - } -} - -pub async fn serve(mut listener: L, agent: A) -> Result<(), Error> -where - S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, - L: Stream> + Unpin, - A: Agent + Send + Sync + 'static, -{ - let keys = KeyStore(Arc::new(RwLock::new(HashMap::new()))); - let lock = Lock(Arc::new(RwLock::new(CryptoVec::new()))); - while let Some(Ok(stream)) = listener.next().await { - russh_util::runtime::spawn( - (Connection { - lock: lock.clone(), - keys: keys.clone(), - agent: Some(agent.clone()), - s: stream, - buf: Vec::new(), - }) - .run(), - ); - } - Ok(()) -} - -impl Agent for () { - fn confirm(self, _: Arc) -> Box + Unpin + Send> { - Box::new(futures::future::ready((self, true))) - } -} - -struct Connection { - lock: Lock, - keys: KeyStore, - agent: Option, - s: S, - buf: Vec, -} - -impl - Connection -{ - async fn read_frame(&mut self) -> Result<(), Error> { - self.buf.clear(); - self.buf.resize(4, 0); - self.s.read_exact(&mut self.buf).await?; - - let len = BigEndian::read_u32(&self.buf) as usize; - if len > MAX_AGENT_FRAME_LEN { - return Err(Error::AgentProtocolError); - } - - self.buf.clear(); - self.buf.resize(len, 0); - self.s.read_exact(&mut self.buf).await?; - Ok(()) - } - - async fn run(mut self) -> Result<(), Error> { - let mut writebuf = Vec::new(); - loop { - self.read_frame().await?; - // respond - writebuf.clear(); - self.respond(&mut writebuf).await?; - self.s.write_all(&writebuf).await?; - self.s.flush().await? - } - } - - async fn respond(&mut self, writebuf: &mut Vec) -> Result<(), Error> { - let is_locked = { - if let Ok(password) = self.lock.0.read() { - !password.is_empty() - } else { - true - } - }; - writebuf.extend_from_slice(&[0, 0, 0, 0]); - let agentref = self.agent.as_ref().ok_or(Error::AgentFailure)?; - - match self.buf.split_first() { - Some((&11, _)) - if !is_locked && agentref.confirm_request(MessageType::RequestKeys).await => - { - // request identities - if let Ok(keys) = self.keys.0.read() { - msg::IDENTITIES_ANSWER.encode(writebuf)?; - (keys.len() as u32).encode(writebuf)?; - for (k, _) in keys.iter() { - k.encode(writebuf)?; - "".encode(writebuf)?; - } - } else { - msg::FAILURE.encode(writebuf)? - } - } - Some((&13, mut r)) - if !is_locked && agentref.confirm_request(MessageType::Sign).await => - { - // sign request - let agent = self.agent.take().ok_or(Error::AgentFailure)?; - let (agent, signed) = self.try_sign(agent, &mut r, writebuf).await?; - self.agent = Some(agent); - if signed { - return Ok(()); - } else { - writebuf.resize(4, 0); - writebuf.push(msg::FAILURE) - } - } - Some((&17, mut r)) - if !is_locked && agentref.confirm_request(MessageType::AddKeys).await => - { - // add identity - if let Ok(true) = self.add_key(&mut r, false, writebuf).await { - } else { - writebuf.push(msg::FAILURE) - } - } - Some((&18, mut r)) - if !is_locked && agentref.confirm_request(MessageType::RemoveKeys).await => - { - // remove identity - if let Ok(true) = self.remove_identity(&mut r) { - writebuf.push(msg::SUCCESS) - } else { - writebuf.push(msg::FAILURE) - } - } - Some((&19, _)) - if !is_locked && agentref.confirm_request(MessageType::RemoveAllKeys).await => - { - // remove all identities - if let Ok(mut keys) = self.keys.0.write() { - keys.clear(); - writebuf.push(msg::SUCCESS) - } else { - writebuf.push(msg::FAILURE) - } - } - Some((&22, mut r)) - if !is_locked && agentref.confirm_request(MessageType::Lock).await => - { - // lock - if let Ok(()) = self.lock(&mut r) { - writebuf.push(msg::SUCCESS) - } else { - writebuf.push(msg::FAILURE) - } - } - Some((&23, mut r)) - if is_locked && agentref.confirm_request(MessageType::Unlock).await => - { - // unlock - if let Ok(true) = self.unlock(&mut r) { - writebuf.push(msg::SUCCESS) - } else { - writebuf.push(msg::FAILURE) - } - } - Some((&25, mut r)) - if !is_locked && agentref.confirm_request(MessageType::AddKeys).await => - { - // add identity constrained - if let Ok(true) = self.add_key(&mut r, true, writebuf).await { - } else { - writebuf.push(msg::FAILURE) - } - } - _ => { - // Message not understood - writebuf.push(msg::FAILURE) - } - } - let len = writebuf.len() - 4; - BigEndian::write_u32(&mut writebuf[..], len as u32); - Ok(()) - } - - fn lock(&self, r: &mut R) -> Result<(), Error> { - let password = Bytes::decode(r)?; - let mut lock = self.lock.0.write().or(Err(Error::AgentFailure))?; - lock.extend(&password); - Ok(()) - } - - fn unlock(&self, r: &mut R) -> Result { - let password = Bytes::decode(r)?; - let mut lock = self.lock.0.write().or(Err(Error::AgentFailure))?; - if lock[..] == password { - lock.clear(); - Ok(true) - } else { - Ok(false) - } - } - - fn remove_identity(&self, r: &mut R) -> Result { - if let Ok(mut keys) = self.keys.0.write() { - if keys.remove(&Bytes::decode(r)?.to_vec()).is_some() { - Ok(true) - } else { - Ok(false) - } - } else { - Ok(false) - } - } - - async fn add_key( - &self, - r: &mut R, - constrained: bool, - writebuf: &mut Vec, - ) -> Result { - let (blob, key_pair) = { - let private_key = - ssh_key::private::PrivateKey::new(ssh_key::private::KeypairData::decode(r)?, "")?; - let _comment = String::decode(r)?; - - (private_key.public_key().key_data().encoded()?, private_key) - }; - writebuf.push(msg::SUCCESS); - let mut w = self.keys.0.write().or(Err(Error::AgentFailure))?; - let now = SystemTime::now(); - if constrained { - let mut c = Vec::new(); - while let Ok(t) = u8::decode(r) { - if t == msg::CONSTRAIN_LIFETIME { - let seconds = u32::decode(r)?; - c.push(Constraint::KeyLifetime { seconds }); - let blob = blob.clone(); - let keys = self.keys.clone(); - russh_util::runtime::spawn(async move { - sleep(Duration::from_secs(seconds as u64)).await; - if let Ok(mut keys) = keys.0.write() { - let delete = if let Some(&(_, time, _)) = keys.get(&blob) { - time == now - } else { - false - }; - if delete { - keys.remove(&blob); - } - } - }); - } else if t == msg::CONSTRAIN_CONFIRM { - c.push(Constraint::Confirm) - } else { - return Ok(false); - } - } - w.insert(blob, (Arc::new(key_pair), now, c)); - } else { - w.insert(blob, (Arc::new(key_pair), now, Vec::new())); - } - Ok(true) - } - - async fn try_sign( - &self, - agent: A, - r: &mut R, - writebuf: &mut Vec, - ) -> Result<(A, bool), Error> { - let mut needs_confirm = false; - let key = { - let blob = Bytes::decode(r)?; - let k = self.keys.0.read().or(Err(Error::AgentFailure))?; - if let Some((key, _, constraints)) = k.get(&blob.to_vec()) { - if constraints.contains(&Constraint::Confirm) { - needs_confirm = true; - } - key.clone() - } else { - return Ok((agent, false)); - } - }; - let agent = if needs_confirm { - let (agent, ok) = { - let _pk = key.clone(); - Box::new(futures::future::ready((agent, true))) - } - .await; - if !ok { - return Ok((agent, false)); - } - agent - } else { - agent - }; - writebuf.push(msg::SIGN_RESPONSE); - let data = Bytes::decode(r)?; - - sign_with_hash_alg(&PrivateKeyWithHashAlg::new(key, None), &data)?.encode(writebuf)?; - - let len = writebuf.len(); - BigEndian::write_u32(writebuf, (len - 4) as u32); - - Ok((agent, true)) - } -} - -#[cfg(test)] -mod tests { - use byteorder::{BigEndian, ByteOrder}; - use tokio::io::AsyncWriteExt; - - use super::{Connection, KeyStore, Lock, MAX_AGENT_FRAME_LEN}; - use crate::keys::Error; - - #[test] - fn oversized_agent_request_is_rejected_before_allocation() -> std::io::Result<()> { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - runtime.block_on(async { - let (server, mut client) = tokio::io::duplex(64); - let connection = Connection { - lock: Lock(std::sync::Arc::new(std::sync::RwLock::new(crate::CryptoVec::new()))), - keys: KeyStore(std::sync::Arc::new(std::sync::RwLock::new( - std::collections::HashMap::new(), - ))), - agent: Some(()), - s: server, - buf: Vec::new(), - }; - let server = tokio::spawn(async move { connection.run().await }); - - let mut frame = [0u8; 4]; - BigEndian::write_u32(&mut frame, (MAX_AGENT_FRAME_LEN + 1) as u32); - client.write_all(&frame).await?; - drop(client); - - let err = server.await.expect("server task").unwrap_err(); - assert!(matches!(err, Error::AgentProtocolError)); - Ok::<(), std::io::Error>(()) - })?; - - Ok(()) - } -} diff --git a/crates/bssh-russh/src/keys/format/mod.rs b/crates/bssh-russh/src/keys/format/mod.rs deleted file mode 100644 index 8a0fcea7..00000000 --- a/crates/bssh-russh/src/keys/format/mod.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::io::Write; - -use data_encoding::{BASE64_MIME, HEXLOWER_PERMISSIVE}; -use ssh_key::PrivateKey; - -use super::is_base64_char; -use crate::keys::Error; - -pub mod openssh; - -#[cfg(feature = "legacy-ed25519-pkcs8-parser")] -mod pkcs8_legacy; - -#[cfg(test)] -mod tests; - -pub use self::openssh::*; - -pub mod pkcs5; -pub use self::pkcs5::*; - -pub mod pkcs8; - -const AES_128_CBC: &str = "DEK-Info: AES-128-CBC,"; - -#[derive(Clone, Copy, Debug)] -/// AES encryption key. -pub enum Encryption { - /// Key for AES128 - Aes128Cbc([u8; 16]), - /// Key for AES256 - Aes256Cbc([u8; 16]), -} - -#[derive(Clone, Debug)] -enum Format { - #[cfg(feature = "rsa")] - Rsa, - Openssh, - Pkcs5Encrypted(Encryption), - Pkcs8Encrypted, - Pkcs8, -} - -/// Decode a secret key, possibly deciphering it with the supplied -/// password. -pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result { - if secret.trim().starts_with("PuTTY-User-Key-File-") { - return Ok(PrivateKey::from_ppk(secret, password.map(Into::into))?); - } - let mut format = None; - let secret = { - let mut started = false; - let mut sec = String::new(); - for l in secret.lines() { - if started { - if l.starts_with("-----END ") { - break; - } - if l.chars().all(is_base64_char) { - sec.push_str(l) - } else if l.starts_with(AES_128_CBC) { - let iv_: Vec = - HEXLOWER_PERMISSIVE.decode(l.split_at(AES_128_CBC.len()).1.as_bytes())?; - if iv_.len() != 16 { - return Err(Error::CouldNotReadKey); - } - let mut iv = [0; 16]; - iv.clone_from_slice(&iv_); - format = Some(Format::Pkcs5Encrypted(Encryption::Aes128Cbc(iv))) - } - } - if l == "-----BEGIN OPENSSH PRIVATE KEY-----" { - started = true; - format = Some(Format::Openssh); - } else if l == "-----BEGIN RSA PRIVATE KEY-----" { - #[cfg(feature = "rsa")] - { - started = true; - format = Some(Format::Rsa); - } - #[cfg(not(feature = "rsa"))] - { - return Err(Error::UnsupportedKeyType { - key_type_string: "RSA".to_string(), - key_type_raw: vec![], - }); - } - } else if l == "-----BEGIN ENCRYPTED PRIVATE KEY-----" { - started = true; - format = Some(Format::Pkcs8Encrypted); - } else if l == "-----BEGIN PRIVATE KEY-----" || l == "-----BEGIN EC PRIVATE KEY-----" { - started = true; - format = Some(Format::Pkcs8); - } - } - sec - }; - - let secret = BASE64_MIME.decode(secret.as_bytes())?; - match format { - Some(Format::Openssh) => decode_openssh(&secret, password), - #[cfg(feature = "rsa")] - Some(Format::Rsa) => Ok(decode_rsa_pkcs1_der(&secret)?.into()), - Some(Format::Pkcs5Encrypted(enc)) => decode_pkcs5(&secret, password, enc), - Some(Format::Pkcs8Encrypted) | Some(Format::Pkcs8) => { - let result = self::pkcs8::decode_pkcs8(&secret, password.map(|x| x.as_bytes())); - #[cfg(feature = "legacy-ed25519-pkcs8-parser")] - { - if result.is_err() { - let legacy_result = - pkcs8_legacy::decode_pkcs8(&secret, password.map(|x| x.as_bytes())); - if let Ok(key) = legacy_result { - return Ok(key); - } - } - } - result - } - None => Err(Error::CouldNotReadKey), - } -} - -pub fn encode_pkcs8_pem(key: &PrivateKey, mut w: W) -> Result<(), Error> { - let x = self::pkcs8::encode_pkcs8(key)?; - w.write_all(b"-----BEGIN PRIVATE KEY-----\n")?; - w.write_all(BASE64_MIME.encode(&x).as_bytes())?; - w.write_all(b"\n-----END PRIVATE KEY-----\n")?; - Ok(()) -} - -pub fn encode_pkcs8_pem_encrypted( - key: &PrivateKey, - pass: &[u8], - rounds: u32, - mut w: W, -) -> Result<(), Error> { - let x = self::pkcs8::encode_pkcs8_encrypted(pass, rounds, key)?; - w.write_all(b"-----BEGIN ENCRYPTED PRIVATE KEY-----\n")?; - w.write_all(BASE64_MIME.encode(&x).as_bytes())?; - w.write_all(b"\n-----END ENCRYPTED PRIVATE KEY-----\n")?; - Ok(()) -} - -#[cfg(feature = "rsa")] -fn decode_rsa_pkcs1_der(secret: &[u8]) -> Result { - use std::convert::TryInto; - - use pkcs1::DecodeRsaPrivateKey; - - Ok(rsa::RsaPrivateKey::from_pkcs1_der(secret)?.try_into()?) -} diff --git a/crates/bssh-russh/src/keys/format/openssh.rs b/crates/bssh-russh/src/keys/format/openssh.rs deleted file mode 100644 index cdcbb98a..00000000 --- a/crates/bssh-russh/src/keys/format/openssh.rs +++ /dev/null @@ -1,17 +0,0 @@ -use ssh_key::PrivateKey; - -use crate::keys::Error; - -/// Decode a secret key given in the OpenSSH format, deciphering it if -/// needed using the supplied password. -pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result { - let pk = PrivateKey::from_bytes(secret)?; - if pk.is_encrypted() { - if let Some(password) = password { - return Ok(pk.decrypt(password)?); - } else { - return Err(Error::KeyIsEncrypted); - } - } - Ok(pk) -} diff --git a/crates/bssh-russh/src/keys/format/pkcs5.rs b/crates/bssh-russh/src/keys/format/pkcs5.rs deleted file mode 100644 index c5bfe861..00000000 --- a/crates/bssh-russh/src/keys/format/pkcs5.rs +++ /dev/null @@ -1,46 +0,0 @@ -use aes::*; -use ssh_key::PrivateKey; - -use super::Encryption; -use crate::keys::Error; - -/// Decode a secret key in the PKCS#5 format, possibly deciphering it -/// using the supplied password. -pub fn decode_pkcs5( - secret: &[u8], - password: Option<&str>, - enc: Encryption, -) -> Result { - use aes::cipher::{block_padding::Pkcs7, BlockModeDecrypt, KeyIvInit}; - - if let Some(pass) = password { - let sec = match enc { - Encryption::Aes128Cbc(ref iv) => { - let mut c = md5::Context::new(); - c.consume(pass.as_bytes()); - c.consume(&iv[..8]); - let md5 = c.compute(); - - #[allow(clippy::unwrap_used)] // AES parameters are static - let c = cbc::Decryptor::::new_from_slices(&md5.0, &iv[..]).unwrap(); - let mut dec = secret.to_vec(); - c.decrypt_padded::(&mut dec)?.to_vec() - } - Encryption::Aes256Cbc(_) => unimplemented!(), - }; - // TODO: presumably pkcs5 could contain non-RSA keys? - #[cfg(feature = "rsa")] - { - super::decode_rsa_pkcs1_der(&sec).map(Into::into) - } - #[cfg(not(feature = "rsa"))] - { - Err(Error::UnsupportedKeyType { - key_type_string: "RSA".to_string(), - key_type_raw: vec![], - }) - } - } else { - Err(Error::KeyIsEncrypted) - } -} diff --git a/crates/bssh-russh/src/keys/format/pkcs8.rs b/crates/bssh-russh/src/keys/format/pkcs8.rs deleted file mode 100644 index 6cd10593..00000000 --- a/crates/bssh-russh/src/keys/format/pkcs8.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::convert::{TryFrom, TryInto}; - -use p256::NistP256; -use p384::NistP384; -use p521::NistP521; -use pkcs8::{AssociatedOid, EncodePrivateKey, PrivateKeyInfoRef, SecretDocument}; -use spki::ObjectIdentifier; -use ssh_key::PrivateKey; -use ssh_key::private::{EcdsaKeypair, Ed25519Keypair, Ed25519PrivateKey, KeypairData}; - -use crate::keys::Error; -use crate::keys::key::safe_rng; - -/// Decode a PKCS#8-encoded private key (ASN.1 or X9.62) -pub fn decode_pkcs8( - ciphertext: &[u8], - password: Option<&[u8]>, -) -> Result { - let doc = SecretDocument::try_from(ciphertext)?; - let doc = if let Some(password) = password { - doc.decode_msg::>()? - .decrypt(password)? - } else { - doc - }; - - match doc.decode_msg::() { - Ok(key) => { - // X9.62 EC private key - let Some(curve) = key.parameters.and_then(|x| x.named_curve()) else { - return Err(Error::CouldNotReadKey); - }; - let kp = ec_key_data_into_keypair(curve, key)?; - Ok(PrivateKey::new(KeypairData::Ecdsa(kp), "")?) - } - Err(_) => { - // ASN.1 key - Ok( - pkcs8_pki_into_keypair_data(doc.decode_msg::>()?)? - .try_into()?, - ) - } - } -} - -fn pkcs8_pki_into_keypair_data(pki: PrivateKeyInfoRef<'_>) -> Result { - // Temporary if {} due to multiple const_oid crate versions - #[cfg(feature = "rsa")] - if pki.algorithm.oid.as_bytes() == pkcs1::ALGORITHM_OID.as_bytes() { - let sk = &pkcs1::RsaPrivateKey::try_from(pki.private_key)?; - let pk = rsa::RsaPrivateKey::from_components( - rsa::BoxedUint::from_be_slice_vartime(sk.modulus.as_bytes()), - rsa::BoxedUint::from_be_slice_vartime(sk.public_exponent.as_bytes()), - rsa::BoxedUint::from_be_slice_vartime(sk.private_exponent.as_bytes()), - vec![ - rsa::BoxedUint::from_be_slice_vartime(sk.prime1.as_bytes()), - rsa::BoxedUint::from_be_slice_vartime(sk.prime2.as_bytes()), - ], - )?; - return Ok(KeypairData::Rsa(pk.try_into()?)); - } - match pki.algorithm.oid { - ed25519_dalek::pkcs8::ALGORITHM_OID => { - let kpb = ed25519_dalek::pkcs8::KeypairBytes::try_from(pki)?; - let pk = Ed25519PrivateKey::from_bytes(&kpb.secret_key); - Ok(KeypairData::Ed25519(Ed25519Keypair { - public: pk.clone().into(), - private: pk, - })) - } - sec1::ALGORITHM_OID => Ok(KeypairData::Ecdsa(ec_key_data_into_keypair( - pki.algorithm.parameters_oid()?, - pki, - )?)), - oid => Err(Error::UnknownAlgorithm(oid)), - } -} - -fn ec_key_data_into_keypair( - curve_oid: ObjectIdentifier, - private_key: K, -) -> Result -where - p256::SecretKey: TryFrom, - p384::SecretKey: TryFrom, - p521::SecretKey: TryFrom, - crate::keys::Error: From, -{ - Ok(match curve_oid { - NistP256::OID => { - let sk = p256::SecretKey::try_from(private_key)?; - EcdsaKeypair::NistP256 { - public: sk.public_key().into(), - private: sk.into(), - } - } - NistP384::OID => { - let sk = p384::SecretKey::try_from(private_key)?; - EcdsaKeypair::NistP384 { - public: sk.public_key().into(), - private: sk.into(), - } - } - NistP521::OID => { - let sk = p521::SecretKey::try_from(private_key)?; - EcdsaKeypair::NistP521 { - public: sk.public_key().into(), - private: sk.into(), - } - } - oid => return Err(Error::UnknownAlgorithm(oid)), - }) -} - -/// Encode into a password-protected PKCS#8-encoded private key. -pub fn encode_pkcs8_encrypted( - pass: &[u8], - rounds: u32, - key: &PrivateKey, -) -> Result, Error> { - let pvi_bytes = encode_pkcs8(key)?; - let pvi = PrivateKeyInfoRef::try_from(pvi_bytes.as_slice())?; - - use rand_core::Rng; - let mut rng = safe_rng(); - let mut salt = [0; 32]; - rng.fill_bytes(&mut salt); - let mut iv = [0; 16]; - rng.fill_bytes(&mut iv); - - let doc = pvi.encrypt_with_params( - pkcs5::pbes2::Parameters::generate_pbkdf2_sha256_aes256cbc(rounds, &salt, iv) - .map_err(|_| Error::InvalidParameters)?, - pass, - )?; - Ok(doc.as_bytes().to_vec()) -} - -/// Encode into a PKCS#8-encoded private key. -pub fn encode_pkcs8(key: &ssh_key::PrivateKey) -> Result, Error> { - let v = match key.key_data() { - ssh_key::private::KeypairData::Ed25519(pair) => { - let sk: ed25519_dalek::SigningKey = pair.try_into()?; - sk.to_pkcs8_der()?.as_bytes().to_vec() - } - #[cfg(feature = "rsa")] - ssh_key::private::KeypairData::Rsa(pair) => { - use rsa::pkcs8::EncodePrivateKey; - let sk: rsa::RsaPrivateKey = pair.try_into()?; - sk.to_pkcs8_der()?.as_bytes().to_vec() - } - ssh_key::private::KeypairData::Ecdsa(pair) => match pair { - EcdsaKeypair::NistP256 { private, .. } => { - let sk = p256::SecretKey::from_slice(private.as_slice())?; - sk.to_pkcs8_der()?.as_bytes().to_vec() - } - EcdsaKeypair::NistP384 { private, .. } => { - let sk = p384::SecretKey::from_slice(private.as_slice())?; - sk.to_pkcs8_der()?.as_bytes().to_vec() - } - EcdsaKeypair::NistP521 { private, .. } => { - let sk = p521::SecretKey::from_slice(private.as_slice())?; - sk.to_pkcs8_der()?.as_bytes().to_vec() - } - }, - _ => { - let algo = key.algorithm(); - let kt = algo.as_str(); - return Err(Error::UnsupportedKeyType { - key_type_string: kt.into(), - key_type_raw: kt.as_bytes().into(), - }); - } - }; - Ok(v) -} diff --git a/crates/bssh-russh/src/keys/format/pkcs8_legacy.rs b/crates/bssh-russh/src/keys/format/pkcs8_legacy.rs deleted file mode 100644 index 099e5d2d..00000000 --- a/crates/bssh-russh/src/keys/format/pkcs8_legacy.rs +++ /dev/null @@ -1,223 +0,0 @@ -use std::borrow::Cow; -use std::convert::TryFrom; - -use aes::cipher::KeyIvInit; -use aes::*; -use block_padding::Pkcs7; -use cipher::BlockModeDecrypt; -use ssh_key::PrivateKey; -use ssh_key::private::{Ed25519Keypair, Ed25519PrivateKey, KeypairData}; -use yasna::BERReaderSeq; - -use super::Encryption; -use crate::keys::Error; - -const PBES2: &[u64] = &[1, 2, 840, 113549, 1, 5, 13]; -const ED25519: &[u64] = &[1, 3, 101, 112]; -const PBKDF2: &[u64] = &[1, 2, 840, 113549, 1, 5, 12]; -const AES256CBC: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 1, 42]; -const HMAC_SHA256: &[u64] = &[1, 2, 840, 113549, 2, 9]; - -pub fn decode_pkcs8(ciphertext: &[u8], password: Option<&[u8]>) -> Result { - let secret = if let Some(pass) = password { - Cow::Owned(yasna::parse_der(ciphertext, |reader| { - reader.read_sequence(|reader| { - // Encryption parameters - let parameters = reader.next().read_sequence(|reader| { - let oid = reader.next().read_oid()?; - if oid.components().as_slice() == PBES2 { - asn1_read_pbes2(reader) - } else { - Ok(Err(Error::InvalidParameters)) - } - })?; - // Ciphertext - let ciphertext = reader.next().read_bytes()?; - Ok(parameters.map(|p| p.decrypt(pass, &ciphertext))) - }) - })???) - } else { - Cow::Borrowed(ciphertext) - }; - yasna::parse_der(&secret, |reader| { - reader.read_sequence(|reader| { - let version = reader.next().read_u64()?; - if version == 0 { - Ok(Err(Error::CouldNotReadKey)) - } else if version == 1 { - Ok(read_key_v1(reader)) - } else { - Ok(Err(Error::CouldNotReadKey)) - } - }) - })? -} - -fn read_key_v1(reader: &mut BERReaderSeq) -> Result { - let oid = reader - .next() - .read_sequence(|reader| reader.next().read_oid())?; - if oid.components().as_slice() == ED25519 { - use ed25519_dalek::SigningKey; - let secret = { - let s = yasna::parse_der(&reader.next().read_bytes()?, |reader| reader.read_bytes())?; - - s.get(..ed25519_dalek::SECRET_KEY_LENGTH) - .ok_or(Error::KeyIsCorrupt) - .and_then(|s| SigningKey::try_from(s).map_err(|_| Error::CouldNotReadKey))? - }; - // Consume the public key - reader - .next() - .read_tagged(yasna::Tag::context(1), |reader| reader.read_bitvec())?; - - let pk = Ed25519PrivateKey::from(&secret); - Ok(PrivateKey::new( - KeypairData::Ed25519(Ed25519Keypair { - public: pk.clone().into(), - private: pk, - }), - "", - )?) - } else { - Err(Error::CouldNotReadKey) - } -} - -#[derive(Debug)] -enum Key { - K128([u8; 16]), - K256([u8; 32]), -} - -impl std::ops::Deref for Key { - type Target = [u8]; - fn deref(&self) -> &[u8] { - match *self { - Key::K128(ref k) => k, - Key::K256(ref k) => k, - } - } -} - -impl std::ops::DerefMut for Key { - fn deref_mut(&mut self) -> &mut [u8] { - match *self { - Key::K128(ref mut k) => k, - Key::K256(ref mut k) => k, - } - } -} - -enum Algorithms { - Pbes2(KeyDerivation, Encryption), -} - -impl Algorithms { - fn decrypt(&self, password: &[u8], cipher: &[u8]) -> Result, Error> { - match *self { - Algorithms::Pbes2(ref der, ref enc) => { - let mut key = enc.key(); - der.derive(password, &mut key)?; - let out = enc.decrypt(&key, cipher)?; - Ok(out) - } - } - } -} - -impl Encryption { - fn key(&self) -> Key { - match *self { - Encryption::Aes128Cbc(_) => Key::K128([0; 16]), - Encryption::Aes256Cbc(_) => Key::K256([0; 32]), - } - } - - fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, Error> { - match *self { - Encryption::Aes128Cbc(ref iv) => { - #[allow(clippy::unwrap_used)] // parameters are static - let c = cbc::Decryptor::::new_from_slices(key, iv).unwrap(); - let mut dec = ciphertext.to_vec(); - Ok(c.decrypt_padded::(&mut dec)?.into()) - } - Encryption::Aes256Cbc(ref iv) => { - #[allow(clippy::unwrap_used)] // parameters are static - let c = cbc::Decryptor::::new_from_slices(key, iv).unwrap(); - let mut dec = ciphertext.to_vec(); - Ok(c.decrypt_padded::(&mut dec)?.into()) - } - } - } -} - -enum KeyDerivation { - Pbkdf2 { salt: Vec, rounds: u64 }, -} - -impl KeyDerivation { - fn derive(&self, password: &[u8], key: &mut [u8]) -> Result<(), Error> { - match *self { - KeyDerivation::Pbkdf2 { ref salt, rounds } => { - pbkdf2::pbkdf2::>(password, salt, rounds as u32, key) - .map_err(|_| Error::InvalidParameters) - // pbkdf2_hmac(password, salt, rounds as usize, digest, key)? - } - } - } -} -fn asn1_read_pbes2( - reader: &mut yasna::BERReaderSeq, -) -> Result, yasna::ASN1Error> { - reader.next().read_sequence(|reader| { - // PBES2 has two components. - // 1. Key generation algorithm - let keygen = reader.next().read_sequence(|reader| { - let oid = reader.next().read_oid()?; - if oid.components().as_slice() == PBKDF2 { - asn1_read_pbkdf2(reader) - } else { - Ok(Err(Error::InvalidParameters)) - } - })?; - // 2. Encryption algorithm. - let algorithm = reader.next().read_sequence(|reader| { - let oid = reader.next().read_oid()?; - if oid.components().as_slice() == AES256CBC { - asn1_read_aes256cbc(reader) - } else { - Ok(Err(Error::InvalidParameters)) - } - })?; - Ok(keygen.and_then(|keygen| algorithm.map(|algo| Algorithms::Pbes2(keygen, algo)))) - }) -} - -fn asn1_read_pbkdf2( - reader: &mut yasna::BERReaderSeq, -) -> Result, yasna::ASN1Error> { - reader.next().read_sequence(|reader| { - let salt = reader.next().read_bytes()?; - let rounds = reader.next().read_u64()?; - let digest = reader.next().read_sequence(|reader| { - let oid = reader.next().read_oid()?; - if oid.components().as_slice() == HMAC_SHA256 { - reader.next().read_null()?; - Ok(Ok(())) - } else { - Ok(Err(Error::InvalidParameters)) - } - })?; - Ok(digest.map(|()| KeyDerivation::Pbkdf2 { salt, rounds })) - }) -} - -fn asn1_read_aes256cbc( - reader: &mut yasna::BERReaderSeq, -) -> Result, yasna::ASN1Error> { - let iv = reader.next().read_bytes()?; - let mut i = [0; 16]; - i.clone_from_slice(&iv); - Ok(Ok(Encryption::Aes256Cbc(i))) -} diff --git a/crates/bssh-russh/src/keys/format/tests.rs b/crates/bssh-russh/src/keys/format/tests.rs deleted file mode 100644 index 6d48a83e..00000000 --- a/crates/bssh-russh/src/keys/format/tests.rs +++ /dev/null @@ -1,26 +0,0 @@ -use ssh_key::{Algorithm, PrivateKey}; - -use crate::keys::pkcs8::{decode_pkcs8, encode_pkcs8_encrypted}; - -use super::decode_secret_key; - -#[test] -fn test_ec_private_key() { - let key = r#"-----BEGIN EC PRIVATE KEY----- -MIGkAgEBBDBNK0jwKqqf8zkM+Z2l++9r8bzdTS/XCoB4N1J07dPxpByyJyGbhvIy -1kLvY2gIvlmgBwYFK4EEACKhZANiAAQvPxAK2RhvH/k5inDa9oMxUZPvvb9fq8G3 -9dKW1tS+ywhejnKeu/48HXAXgx2g6qMJjEPpcTy/DaYm12r3GTaRzOBQmxSItStk -lpQg5vf23Fc9fFrQ9AnQKrb1dgTkoxQ= ------END EC PRIVATE KEY-----"#; - decode_secret_key(key, None).unwrap(); -} - -#[test] -fn test_pkcs8_roundtrip() { - let password = b"SomePassword"; - let original_key = PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519).unwrap(); - - let encrypted = encode_pkcs8_encrypted(password, 10, &original_key).unwrap(); - let decrypted = decode_pkcs8(&encrypted, Some(password)).unwrap(); - assert_eq!(decrypted, original_key); -} diff --git a/crates/bssh-russh/src/keys/key.rs b/crates/bssh-russh/src/keys/key.rs deleted file mode 100644 index de82cb13..00000000 --- a/crates/bssh-russh/src/keys/key.rs +++ /dev/null @@ -1,125 +0,0 @@ -use rand::rng; -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -use ssh_encoding::Decode; -use ssh_key::public::KeyData; -use ssh_key::{Algorithm, EcdsaCurve, PublicKey}; - -use crate::keys::Error; - -pub trait PublicKeyExt { - fn decode(bytes: &[u8]) -> Result; -} - -impl PublicKeyExt for PublicKey { - fn decode(mut bytes: &[u8]) -> Result { - let key = KeyData::decode(&mut bytes)?; - Ok(PublicKey::new(key, "")) - } -} - -#[doc(hidden)] -pub trait Verify { - fn verify_client_auth(&self, buffer: &[u8], sig: &[u8]) -> bool; - fn verify_server_auth(&self, buffer: &[u8], sig: &[u8]) -> bool; -} - -/// Parse a public key from a byte slice. -pub fn parse_public_key(mut p: &[u8]) -> Result { - Ok(ssh_key::public::KeyData::decode(&mut p)?.into()) -} - -/// Obtain a cryptographic-safe random number generator. -pub fn safe_rng() -> impl rand::CryptoRng { - rng() -} - -mod private_key_with_hash_alg { - use std::ops::Deref; - use std::sync::Arc; - - use ssh_key::Algorithm; - - use crate::helpers::AlgorithmExt; - - /// Helper structure to correlate a key and (in case of RSA) a hash algorithm. - /// Only used for authentication, not key storage as RSA keys do not inherently - /// have a hash algorithm associated with them. - #[derive(Clone, Debug)] - pub struct PrivateKeyWithHashAlg { - key: Arc, - hash_alg: Option, - } - - impl PrivateKeyWithHashAlg { - /// Direct constructor. - /// - /// For RSA, passing `None` is mapped to the legacy `sha-rsa` (SHA-1). - /// For other keys, `hash_alg` is ignored. - pub fn new( - key: Arc, - mut hash_alg: Option, - ) -> Self { - if !key.algorithm().is_rsa() { - hash_alg = None; - } - Self { key, hash_alg } - } - - pub fn algorithm(&self) -> Algorithm { - self.key.algorithm().with_hash_alg(self.hash_alg) - } - - pub fn hash_alg(&self) -> Option { - self.hash_alg - } - } - - impl Deref for PrivateKeyWithHashAlg { - type Target = crate::keys::PrivateKey; - - fn deref(&self) -> &Self::Target { - &self.key - } - } -} - -pub use private_key_with_hash_alg::PrivateKeyWithHashAlg; - -pub const ALL_KEY_TYPES: &[Algorithm] = &[ - Algorithm::Dsa, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP256, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP384, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP521, - }, - Algorithm::Ed25519, - #[cfg(feature = "rsa")] - Algorithm::Rsa { hash: None }, - #[cfg(feature = "rsa")] - Algorithm::Rsa { - hash: Some(ssh_key::HashAlg::Sha256), - }, - #[cfg(feature = "rsa")] - Algorithm::Rsa { - hash: Some(ssh_key::HashAlg::Sha512), - }, - Algorithm::SkEcdsaSha2NistP256, - Algorithm::SkEd25519, -]; diff --git a/crates/bssh-russh/src/keys/known_hosts.rs b/crates/bssh-russh/src/keys/known_hosts.rs deleted file mode 100644 index 00d69638..00000000 --- a/crates/bssh-russh/src/keys/known_hosts.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::borrow::Cow; -use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; -use std::path::{Path, PathBuf}; -use std::env; - -use data_encoding::BASE64_MIME; -use hmac::{Hmac, KeyInit, Mac}; -use log::debug; -use sha1::Sha1; - -use crate::keys::Error; - -/// Check whether the host is known, from its standard location. -pub fn check_known_hosts( - host: &str, - port: u16, - pubkey: &ssh_key::PublicKey, -) -> Result { - check_known_hosts_path(host, port, pubkey, known_hosts_path()?) -} - -/// Check that a server key matches the one recorded in file `path`. -pub fn check_known_hosts_path>( - host: &str, - port: u16, - pubkey: &ssh_key::PublicKey, - path: P, -) -> Result { - let check = known_host_keys_path(host, port, path)? - .into_iter() - .map(|(line, recorded)| { - match ( - pubkey.algorithm() == recorded.algorithm(), - *pubkey == recorded, - ) { - (true, true) => Ok(true), - (true, false) => Err(Error::KeyChanged { line }), - _ => Ok(false), - } - }) - // If any Err was returned, we stop here - .collect::, Error>>()? - .into_iter() - // Now we check the results for a match - .any(|x| x); - - Ok(check) -} - -fn known_hosts_path() -> Result { - env::home_dir() - .map(|home_dir| home_dir.join(".ssh").join("known_hosts")) - .ok_or(Error::NoHomeDir) -} - -/// Get the server key that matches the one recorded in the user's known_hosts file. -pub fn known_host_keys(host: &str, port: u16) -> Result, Error> { - known_host_keys_path(host, port, known_hosts_path()?) -} - -/// Get the server key that matches the one recorded in `path`. -pub fn known_host_keys_path>( - host: &str, - port: u16, - path: P, -) -> Result, Error> { - use crate::keys::parse_public_key_base64; - - let mut f = if let Ok(f) = File::open(path) { - BufReader::new(f) - } else { - return Ok(vec![]); - }; - let mut buffer = String::new(); - - let host_port = if port == 22 { - Cow::Borrowed(host) - } else { - Cow::Owned(format!("[{host}]:{port}")) - }; - debug!("host_port = {host_port:?}"); - let mut line = 1; - let mut matches = vec![]; - while f.read_line(&mut buffer)? > 0 { - { - if buffer.as_bytes().first() == Some(&b'#') { - buffer.clear(); - continue; - } - debug!("line = {buffer:?}"); - let mut s = buffer.split(' '); - let hosts = s.next(); - let _ = s.next(); - let key = s.next(); - if let (Some(h), Some(k)) = (hosts, key) { - debug!("{h:?} {k:?}"); - if match_hostname(&host_port, h) { - matches.push((line, parse_public_key_base64(k)?)); - } - } - } - buffer.clear(); - line += 1; - } - Ok(matches) -} - -fn match_hostname(host: &str, pattern: &str) -> bool { - for entry in pattern.split(',') { - if entry.starts_with("|1|") { - let mut parts = entry.split('|').skip(2); - let Some(Ok(salt)) = parts.next().map(|p| BASE64_MIME.decode(p.as_bytes())) else { - continue; - }; - let Some(Ok(hash)) = parts.next().map(|p| BASE64_MIME.decode(p.as_bytes())) else { - continue; - }; - if let Ok(hmac) = Hmac::::new_from_slice(&salt) { - if hmac.chain_update(host).verify_slice(&hash).is_ok() { - return true; - } - } - } else if host == entry { - return true; - } - } - false -} - -/// Record a host's public key into the user's known_hosts file. -pub fn learn_known_hosts(host: &str, port: u16, pubkey: &ssh_key::PublicKey) -> Result<(), Error> { - learn_known_hosts_path(host, port, pubkey, known_hosts_path()?) -} - -/// Record a host's public key into a nonstandard location. -pub fn learn_known_hosts_path>( - host: &str, - port: u16, - pubkey: &ssh_key::PublicKey, - path: P, -) -> Result<(), Error> { - if let Some(parent) = path.as_ref().parent() { - std::fs::create_dir_all(parent)? - } - let mut file = OpenOptions::new() - .read(true) - .append(true) - .create(true) - .open(path)?; - - // Test whether the known_hosts file ends with a \n - let mut buf = [0; 1]; - let mut ends_in_newline = false; - if file.seek(SeekFrom::End(-1)).is_ok() { - file.read_exact(&mut buf)?; - ends_in_newline = buf[0] == b'\n'; - } - - // Write the key. - file.seek(SeekFrom::End(0))?; - let mut file = std::io::BufWriter::new(file); - if !ends_in_newline { - file.write_all(b"\n")?; - } - if port != 22 { - write!(file, "[{host}]:{port} ")? - } else { - write!(file, "{host} ")? - } - file.write_all(pubkey.to_openssh()?.as_bytes())?; - file.write_all(b"\n")?; - Ok(()) -} - -#[cfg(test)] -mod test { - use std::fs::File; - - use super::*; - use crate::keys::parse_public_key_base64; - - #[test] - fn test_check_known_hosts() { - env_logger::try_init().unwrap_or(()); - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("known_hosts"); - { - let mut f = File::create(&path).unwrap(); - f.write_all(b"[localhost]:13265 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ\n").unwrap(); - f.write_all(b"#pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap(); - f.write_all(b"pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap(); - f.write_all(b"|1|O33ESRMWPVkMYIwJ1Uw+n877jTo=|nuuC5vEqXlEZ/8BXQR7m619W6Ak= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILIG2T/B0l0gaqj3puu510tu9N1OkQ4znY3LYuEm5zCF\n").unwrap(); - } - - // Valid key, non-standard port. - let host = "localhost"; - let port = 13265; - let hostkey = parse_public_key_base64( - "AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ", - ) - .unwrap(); - assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap()); - - // Valid key, hashed. - let host = "example.com"; - let port = 22; - let hostkey = parse_public_key_base64( - "AAAAC3NzaC1lZDI1NTE5AAAAILIG2T/B0l0gaqj3puu510tu9N1OkQ4znY3LYuEm5zCF", - ) - .unwrap(); - assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap()); - - // Valid key, several hosts, port 22 - let host = "pijul.org"; - let port = 22; - let hostkey = parse_public_key_base64( - "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X", - ) - .unwrap(); - assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap()); - - // Now with the key in a comment above, check that it's not recognized - let host = "pijul.org"; - let port = 22; - let hostkey = parse_public_key_base64( - "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X", - ) - .unwrap(); - assert!(check_known_hosts_path(host, port, &hostkey, &path).is_err()); - } -} diff --git a/crates/bssh-russh/src/keys/mod.rs b/crates/bssh-russh/src/keys/mod.rs deleted file mode 100644 index 01344dc7..00000000 --- a/crates/bssh-russh/src/keys/mod.rs +++ /dev/null @@ -1,1566 +0,0 @@ -//! This crate contains methods to deal with SSH keys, as defined in -//! crate Russh. This includes in particular various functions for -//! opening key files, deciphering encrypted keys, and dealing with -//! agents. -//! -//! The following example shows how to do all these in a single example: -//! start and SSH agent server, connect to it with a client, decipher -//! an encrypted ED25519 private key (the password is `b"blabla"`), send it to -//! the agent, and ask the agent to sign a piece of data -//! (`b"Please sign this"`, below). -//! -//!``` -//! use russh::keys::*; -//! use futures::Future; -//! -//! #[derive(Clone)] -//! struct X{} -//! impl agent::server::Agent for X { -//! fn confirm(self, _: std::sync::Arc) -> Box + Send + Unpin> { -//! Box::new(futures::future::ready((self, true))) -//! } -//! } -//! -//! const PKCS8_ENCRYPTED: &'static str = "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBAWQiUHKoocuxfoZ/hF\nYTjkAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ83d1d5/S2wz475uC\nCUrE7QRAvdVpD5e3zKH/MZjilWrMOm6cyI1LKBCssLztPyvOALtroLAPlp7WYWfu\n9Sncmm7u14n2lia7r1r5I3VBsVuH0g==\n-----END ENCRYPTED PRIVATE KEY-----\n"; -//! -//! #[cfg(unix)] -//! fn main() { -//! env_logger::try_init().unwrap_or(()); -//! let dir = tempfile::tempdir().unwrap(); -//! let agent_path = dir.path().join("agent"); -//! -//! let mut core = tokio::runtime::Runtime::new().unwrap(); -//! let agent_path_ = agent_path.clone(); -//! // Starting a server -//! core.spawn(async move { -//! let mut listener = tokio::net::UnixListener::bind(&agent_path_) -//! .unwrap(); -//! russh::keys::agent::server::serve(tokio_stream::wrappers::UnixListenerStream::new(listener), X {}).await -//! }); -//! let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); -//! let public = key.public_key().clone(); -//! core.block_on(async move { -//! let stream = tokio::net::UnixStream::connect(&agent_path).await?; -//! let mut client = agent::client::AgentClient::connect(stream); -//! client.add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]).await?; -//! client.request_identities().await?; -//! let buf = b"signed message".to_vec(); -//! let sig = client.sign_request(&public.into(), None, buf).await.unwrap(); -//! // Here, `sig` is encoded in a format usable internally by the SSH protocol. -//! Ok::<(), Error>(()) -//! }).unwrap() -//! } -//! -//! #[cfg(not(unix))] -//! fn main() {} -//! -//! ``` - -use std::fs::File; -use std::io::Read; -use std::path::Path; -use std::string::FromUtf8Error; - -use aes::cipher::inout::PadError; -use data_encoding::BASE64_MIME; -use thiserror::Error; - -use crate::helpers::EncodedExt; - -pub mod key; -pub use key::PrivateKeyWithHashAlg; - -mod format; -pub use format::*; -// Reexports -pub use signature; -pub use ssh_encoding; -pub use ssh_key::{self, Algorithm, Certificate, EcdsaCurve, HashAlg, PrivateKey, PublicKey}; - -/// OpenSSH agent protocol implementation -pub mod agent; - -#[cfg(not(target_arch = "wasm32"))] -pub mod known_hosts; - -#[cfg(not(target_arch = "wasm32"))] -pub use known_hosts::{check_known_hosts, check_known_hosts_path}; - -#[derive(Debug, Error)] -pub enum Error { - /// The key could not be read, for an unknown reason - #[error("Could not read key")] - CouldNotReadKey, - /// The type of the key is unsupported - #[error("Unsupported key type {}", key_type_string)] - UnsupportedKeyType { - key_type_string: String, - key_type_raw: Vec, - }, - /// The type of the key is unsupported - #[error("Invalid Ed25519 key data")] - Ed25519KeyError(#[from] ed25519_dalek::SignatureError), - /// The type of the key is unsupported - #[error("Invalid ECDSA key data")] - EcdsaKeyError(#[from] p256::elliptic_curve::Error), - /// The key is encrypted (should supply a password?) - #[error("The key is encrypted")] - KeyIsEncrypted, - /// The key contents are inconsistent - #[error("The key is corrupt")] - KeyIsCorrupt, - /// Home directory could not be found - #[error("No home directory found")] - NoHomeDir, - /// The server key has changed - #[error("The server key changed at line {}", line)] - KeyChanged { line: usize }, - /// The key uses an unsupported algorithm - #[error("Unknown key algorithm: {0}")] - UnknownAlgorithm(::pkcs8::ObjectIdentifier), - /// Index out of bounds - #[error("Index out of bounds")] - IndexOutOfBounds, - /// Unknown signature type - #[error("Unknown signature type: {}", sig_type)] - UnknownSignatureType { sig_type: String }, - #[error("Invalid signature")] - InvalidSignature, - #[error("Invalid parameters")] - InvalidParameters, - /// Agent protocol error - #[error("Agent protocol error")] - AgentProtocolError, - #[error("Agent failure")] - AgentFailure, - #[error(transparent)] - IO(#[from] std::io::Error), - - #[cfg(feature = "rsa")] - #[error("Rsa: {0}")] - Rsa(#[from] rsa::Error), - - #[error(transparent)] - Pad(#[from] PadError), - - #[error(transparent)] - Unpad(#[from] aes::cipher::block_padding::Error), - - #[error("Base64 decoding error: {0}")] - Decode(#[from] data_encoding::DecodeError), - #[error("Der: {0}")] - Der(#[from] der::Error), - #[error("Spki: {0}")] - Spki(#[from] spki::Error), - #[cfg(feature = "rsa")] - #[error("Pkcs1: {0}")] - Pkcs1(#[from] pkcs1::Error), - #[error("Pkcs8: {0}")] - Pkcs8(#[from] ::pkcs8::Error), - #[error("Sec1: {0}")] - Sec1(#[from] sec1::Error), - - #[error("SshKey: {0}")] - SshKey(#[from] ssh_key::Error), - #[error("SshEncoding: {0}")] - SshEncoding(#[from] ssh_encoding::Error), - - #[error("Environment variable `{0}` not found")] - EnvVar(&'static str), - #[error( - "Unable to connect to ssh-agent. The environment variable `SSH_AUTH_SOCK` was set, but it \ - points to a nonexistent file or directory." - )] - BadAuthSock, - - #[error(transparent)] - Utf8(#[from] FromUtf8Error), - - #[error("ASN1 decoding error: {0}")] - #[cfg(feature = "legacy-ed25519-pkcs8-parser")] - LegacyASN1(::yasna::ASN1Error), - - #[cfg(windows)] - #[error("Pageant: {0}")] - Pageant(#[from] pageant::Error), -} - -#[cfg(feature = "legacy-ed25519-pkcs8-parser")] -impl From for Error { - fn from(e: yasna::ASN1Error) -> Error { - Error::LegacyASN1(e) - } -} - -/// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported. -/// -/// ``` -/// russh::keys::load_public_key("../files/id_ed25519.pub").unwrap(); -/// ``` -pub fn load_public_key>(path: P) -> Result { - let mut pubkey = String::new(); - let mut file = File::open(path.as_ref())?; - file.read_to_string(&mut pubkey)?; - - let mut split = pubkey.split_whitespace(); - match (split.next(), split.next()) { - (Some(_), Some(key)) => parse_public_key_base64(key), - (Some(key), None) => parse_public_key_base64(key), - _ => Err(Error::CouldNotReadKey), - } -} - -/// Reads a public key from the standard encoding. In some cases, the -/// encoding is prefixed with a key type identifier and a space (such -/// as `ssh-ed25519 AAAAC3N...`). -/// -/// ``` -/// russh::keys::parse_public_key_base64("AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ").is_ok(); -/// ``` -pub fn parse_public_key_base64(key: &str) -> Result { - let base = BASE64_MIME.decode(key.as_bytes())?; - key::parse_public_key(&base) -} - -pub trait PublicKeyBase64 { - /// Create the base64 part of the public key blob. - fn public_key_bytes(&self) -> Vec; - fn public_key_base64(&self) -> String { - let mut s = BASE64_MIME.encode(&self.public_key_bytes()); - assert_eq!(s.pop(), Some('\n')); - assert_eq!(s.pop(), Some('\r')); - s.replace("\r\n", "") - } -} - -impl PublicKeyBase64 for ssh_key::PublicKey { - fn public_key_bytes(&self) -> Vec { - self.key_data().encoded().unwrap_or_default() - } -} - -impl PublicKeyBase64 for PrivateKey { - fn public_key_bytes(&self) -> Vec { - self.public_key().public_key_bytes() - } -} - -/// Load a secret key, deciphering it with the supplied password if necessary. -pub fn load_secret_key>( - secret_: P, - password: Option<&str>, -) -> Result { - let mut secret_file = std::fs::File::open(secret_)?; - let mut secret = String::new(); - secret_file.read_to_string(&mut secret)?; - decode_secret_key(&secret, password) -} - -/// Load a openssh certificate -pub fn load_openssh_certificate>(cert_: P) -> Result { - let mut cert_file = std::fs::File::open(cert_)?; - let mut cert = String::new(); - cert_file.read_to_string(&mut cert)?; - - Certificate::from_openssh(&cert) -} - -fn is_base64_char(c: char) -> bool { - c.is_ascii_lowercase() - || c.is_ascii_uppercase() - || c.is_ascii_digit() - || c == '/' - || c == '+' - || c == '=' -} - -#[cfg(test)] -mod test { - - #[cfg(unix)] - use futures::Future; - - use super::*; - #[cfg(unix)] - use crate::keys::agent::AgentIdentity; - use crate::keys::key::PublicKeyExt; - - const ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDLGyfA39 -J2FcJygtYqi5ISAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIN+Wjn4+4Fcvl2Jl -KpggT+wCRxpSvtqqpVrQrKN1/A22AAAAkOHDLnYZvYS6H9Q3S3Nk4ri3R2jAZlQlBbUos5 -FkHpYgNw65KCWCTXtP7ye2czMC3zjn2r98pJLobsLYQgRiHIv/CUdAdsqbvMPECB+wl/UQ -e+JpiSq66Z6GIt0801skPh20jxOO3F52SoX1IeO5D5PXfZrfSZlw6S8c7bwyp2FHxDewRx -7/wNsnDM0T7nLv/Q== ------END OPENSSH PRIVATE KEY-----"; - - // password is 'test' - const ED25519_AESCTR_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD1phlku5 -A2G7Q9iP+DcOc9AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHeLC1lWiCYrXsf/ -85O/pkbUFZ6OGIt49PX3nw8iRoXEAAAAkKRF0st5ZI7xxo9g6A4m4l6NarkQre3mycqNXQ -dP3jryYgvsCIBAA5jMWSjrmnOTXhidqcOy4xYCrAttzSnZ/cUadfBenL+DQq6neffw7j8r -0tbCxVGp6yCQlKrgSZf6c0Hy7dNEIU2bJFGxLe6/kWChcUAt/5Ll5rI7DVQPJdLgehLzvv -sJWR7W+cGvJ/vLsw== ------END OPENSSH PRIVATE KEY-----"; - - #[cfg(feature = "rsa")] - const RSA_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEAuSvQ9m76zhRB4m0BUKPf17lwccj7KQ1Qtse63AOqP/VYItqEH8un -rxPogXNBgrcCEm/ccLZZsyE3qgp3DRQkkqvJhZ6O8VBPsXxjZesRCqoFNCczy+Mf0R/Qmv -Rnpu5+4DDLz0p7vrsRZW9ji/c98KzxeUonWgkplQaCBYLN875WdeUYMGtb1MLfNCEj177j -gZl3CzttLRK3su6dckowXcXYv1gPTPZAwJb49J43o1QhV7+1zdwXvuFM6zuYHdu9ZHSKir -6k1dXOET3/U+LWG5ofAo8oxUWv/7vs6h7MeajwkUeIBOWYtD+wGYRvVpxvj7nyOoWtg+jm -0X6ndnsD+QAAA8irV+ZAq1fmQAAAAAdzc2gtcnNhAAABAQC5K9D2bvrOFEHibQFQo9/XuX -BxyPspDVC2x7rcA6o/9Vgi2oQfy6evE+iBc0GCtwISb9xwtlmzITeqCncNFCSSq8mFno7x -UE+xfGNl6xEKqgU0JzPL4x/RH9Ca9Gem7n7gMMvPSnu+uxFlb2OL9z3wrPF5SidaCSmVBo -IFgs3zvlZ15Rgwa1vUwt80ISPXvuOBmXcLO20tErey7p1ySjBdxdi/WA9M9kDAlvj0njej -VCFXv7XN3Be+4UzrO5gd271kdIqKvqTV1c4RPf9T4tYbmh8CjyjFRa//u+zqHsx5qPCRR4 -gE5Zi0P7AZhG9WnG+PufI6ha2D6ObRfqd2ewP5AAAAAwEAAQAAAQAdELqhI/RsSpO45eFR -9hcZtnrm8WQzImrr9dfn1w9vMKSf++rHTuFIQvi48Q10ZiOGH1bbvlPAIVOqdjAPtnyzJR -HhzmyjhjasJlk30zj+kod0kz63HzSMT9EfsYNfmYoCyMYFCKz52EU3xc87Vhi74XmZz0D0 -CgIj6TyZftmzC4YJCiwwU8K+29nxBhcbFRxpgwAksFL6PCSQsPl4y7yvXGcX+7lpZD8547 -v58q3jIkH1g2tBOusIuaiphDDStVJhVdKA55Z0Kju2kvCqsRIlf1efrq43blRgJFFFCxNZ -8Cpolt4lOHhg+o3ucjILlCOgjDV8dB21YLxmgN5q+xFNAAAAgQC1P+eLUkHDFXnleCEVrW -xL/DFxEyneLQz3IawGdw7cyAb7vxsYrGUvbVUFkxeiv397pDHLZ5U+t5cOYDBZ7G43Mt2g -YfWBuRNvYhHA9Sdf38m5qPA6XCvm51f+FxInwd/kwRKH01RHJuRGsl/4Apu4DqVob8y00V -WTYyV6JBNDkQAAAIEA322lj7ZJXfK/oLhMM/RS+DvaMea1g/q43mdRJFQQso4XRCL6IIVn -oZXFeOxrMIRByVZBw+FSeB6OayWcZMySpJQBo70GdJOc3pJb3js0T+P2XA9+/jwXS58K9a -+IkgLkv9XkfxNGNKyPEEzXC8QQzvjs1LbmO59VLko8ypwHq/cAAACBANQqaULI0qdwa0vm -d3Ae1+k3YLZ0kapSQGVIMT2lkrhKV35tj7HIFpUPa4vitHzcUwtjYhqFezVF+JyPbJ/Fsp -XmEc0g1fFnQp5/SkUwoN2zm8Up52GBelkq2Jk57mOMzWO0QzzNuNV/feJk02b2aE8rrAqP -QR+u0AypRPmzHnOPAAAAEXJvb3RAMTQwOTExNTQ5NDBkAQ== ------END OPENSSH PRIVATE KEY-----"; - - #[test] - fn test_decode_ed25519_secret_key() { - env_logger::try_init().unwrap_or(()); - decode_secret_key(ED25519_KEY, Some("blabla")).unwrap(); - } - - #[test] - fn test_decode_ed25519_aesctr_secret_key() { - env_logger::try_init().unwrap_or(()); - decode_secret_key(ED25519_AESCTR_KEY, Some("test")).unwrap(); - } - - // Key from RFC 8410 Section 10.3. This is a key using PrivateKeyInfo structure. - const RFC8410_ED25519_PRIVATE_ONLY_KEY: &str = "-----BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC ------END PRIVATE KEY-----"; - - #[test] - fn test_decode_rfc8410_ed25519_private_only_key() { - env_logger::try_init().unwrap_or(()); - assert!( - decode_secret_key(RFC8410_ED25519_PRIVATE_ONLY_KEY, None) - .unwrap() - .algorithm() - == ssh_key::Algorithm::Ed25519, - ); - // We always encode public key, skip test_decode_encode_symmetry. - } - - // Key from RFC 8410 Section 10.3. This is a key using OneAsymmetricKey structure. - const RFC8410_ED25519_PRIVATE_PUBLIC_KEY: &str = "-----BEGIN PRIVATE KEY----- -MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC -oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB -Z9w7lshQhqowtrbLDFw4rXAxZuE= ------END PRIVATE KEY-----"; - - #[test] - fn test_decode_rfc8410_ed25519_private_public_key() { - env_logger::try_init().unwrap_or(()); - assert!( - decode_secret_key(RFC8410_ED25519_PRIVATE_PUBLIC_KEY, None) - .unwrap() - .algorithm() - == ssh_key::Algorithm::Ed25519, - ); - // We can't encode attributes, skip test_decode_encode_symmetry. - } - - #[cfg(feature = "rsa")] - #[test] - fn test_decode_rsa_secret_key() { - env_logger::try_init().unwrap_or(()); - decode_secret_key(RSA_KEY, None).unwrap(); - } - - #[test] - fn test_decode_openssh_p256_secret_key() { - // Generated using: ssh-keygen -t ecdsa -b 256 -m rfc4716 -f $file - let key = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS -1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQ/i+HCsmZZPy0JhtT64vW7EmeA1DeA -M/VnPq3vAhu+xooJ7IMMK3lUHlBDosyvA2enNbCWyvNQc25dVt4oh9RhAAAAqHG7WMFxu1 -jBAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBD+L4cKyZlk/LQmG -1Pri9bsSZ4DUN4Az9Wc+re8CG77GignsgwwreVQeUEOizK8DZ6c1sJbK81Bzbl1W3iiH1G -EAAAAgLAmXR6IlN0SdiD6o8qr+vUr0mXLbajs/m0UlegElOmoAAAANcm9iZXJ0QGJic2Rl -dgECAw== ------END OPENSSH PRIVATE KEY----- -"; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP256 - }, - ); - } - - #[test] - fn test_decode_openssh_p384_secret_key() { - // Generated using: ssh-keygen -t ecdsa -b 384 -m rfc4716 -f $file - let key = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS -1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTkLnKPk/1NZD9mQ8XoebD7ASv9/svh -5jO75HF7RYAqKK3fl5wsHe4VTJAOT3qH841yTcK79l0dwhHhHeg60byL7F9xOEzr2kqGeY -Uwrl7fVaL7hfHzt6z+sG8smSQ3tF8AAADYHjjBch44wXIAAAATZWNkc2Etc2hhMi1uaXN0 -cDM4NAAAAAhuaXN0cDM4NAAAAGEE5C5yj5P9TWQ/ZkPF6Hmw+wEr/f7L4eYzu+Rxe0WAKi -it35ecLB3uFUyQDk96h/ONck3Cu/ZdHcIR4R3oOtG8i+xfcThM69pKhnmFMK5e31Wi+4Xx -87es/rBvLJkkN7RfAAAAMFzt6053dxaQT0Ta/CGfZna0nibHzxa55zgBmje/Ho3QDNlBCH -Ylv0h4Wyzto8NfLQAAAA1yb2JlcnRAYmJzZGV2AQID ------END OPENSSH PRIVATE KEY----- -"; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP384 - }, - ); - } - - #[test] - fn test_decode_openssh_p521_secret_key() { - // Generated using: ssh-keygen -t ecdsa -b 521 -m rfc4716 -f $file - let key = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS -1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQA7a9awmFeDjzYiuUOwMfXkKTevfQI -iGlduu8BkjBOWXpffJpKsdTyJI/xI05l34OvqfCCkPUcfFWHK+LVRGahMBgBcGB9ZZOEEq -iKNIT6C9WcJTGDqcBSzQ2yTSOxPXfUmVTr4D76vbYu5bjd9aBKx8HdfMvPeo0WD0ds/LjX -LdJoDXcAAAEQ9fxlIfX8ZSEAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ -AAAIUEAO2vWsJhXg482IrlDsDH15Ck3r30CIhpXbrvAZIwTll6X3yaSrHU8iSP8SNOZd+D -r6nwgpD1HHxVhyvi1URmoTAYAXBgfWWThBKoijSE+gvVnCUxg6nAUs0Nsk0jsT131JlU6+ -A++r22LuW43fWgSsfB3XzLz3qNFg9HbPy41y3SaA13AAAAQgH4DaftY0e/KsN695VJ06wy -Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA -1yb2JlcnRAYmJzZGV2AQIDBAU= ------END OPENSSH PRIVATE KEY----- -"; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP521 - }, - ); - } - - #[test] - fn test_fingerprint() { - let key = parse_public_key_base64( - "AAAAC3NzaC1lZDI1NTE5AAAAILagOJFgwaMNhBWQINinKOXmqS4Gh5NgxgriXwdOoINJ", - ) - .unwrap(); - assert_eq!( - format!("{}", key.fingerprint(ssh_key::HashAlg::Sha256)), - "SHA256:ldyiXa1JQakitNU5tErauu8DvWQ1dZ7aXu+rm7KQuog" - ); - } - - #[test] - fn test_parse_p256_public_key() { - env_logger::try_init().unwrap_or(()); - let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMxBTpMIGvo7CnordO7wP0QQRqpBwUjOLl4eMhfucfE1sjTYyK5wmTl1UqoSDS1PtRVTBdl+0+9pquFb46U7fwg="; - - assert!( - parse_public_key_base64(key).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP256 - }, - ); - } - - #[test] - fn test_parse_p384_public_key() { - env_logger::try_init().unwrap_or(()); - let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBVFgxJxpCaAALZG/S5BHT8/IUQ5mfuKaj7Av9g7Jw59fBEGHfPBz1wFtHGYw5bdLmfVZTIDfogDid5zqJeAKr1AcD06DKTXDzd2EpUjqeLfQ5b3erHuX758fgu/pSDGRA=="; - - assert!( - parse_public_key_base64(key).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP384 - } - ); - } - - #[test] - fn test_parse_p521_public_key() { - env_logger::try_init().unwrap_or(()); - let key = "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAQepXEpOrzlX22r4E5zEHjhHWeZUe//zaevTanOWRBnnaCGWJFGCdjeAbNOuAmLtXc+HZdJTCZGREeSLSrpJa71QDCgZl0N7DkDUanCpHZJe/DCK6qwtHYbEMn28iLMlGCOrCIa060EyJHbp1xcJx4I1SKj/f/fm3DhhID/do6zyf8Cg=="; - - assert!( - parse_public_key_base64(key).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP521 - } - ); - } - - #[test] - fn test_srhb() { - env_logger::try_init().unwrap_or(()); - let key = "AAAAB3NzaC1yc2EAAAADAQABAAACAQC0Xtz3tSNgbUQAXem4d+d6hMx7S8Nwm/DOO2AWyWCru+n/+jQ7wz2b5+3oG2+7GbWZNGj8HCc6wJSA3jUsgv1N6PImIWclD14qvoqY3Dea1J0CJgXnnM1xKzBz9C6pDHGvdtySg+yzEO41Xt4u7HFn4Zx5SGuI2NBsF5mtMLZXSi33jCIWVIkrJVd7sZaY8jiqeVZBB/UvkLPWewGVuSXZHT84pNw4+S0Rh6P6zdNutK+JbeuO+5Bav4h9iw4t2sdRkEiWg/AdMoSKmo97Gigq2mKdW12ivnXxz3VfxrCgYJj9WwaUUWSfnAju5SiNly0cTEAN4dJ7yB0mfLKope1kRhPsNaOuUmMUqlu/hBDM/luOCzNjyVJ+0LLB7SV5vOiV7xkVd4KbEGKou8eeCR3yjFazUe/D1pjYPssPL8cJhTSuMc+/UC9zD8yeEZhB9V+vW4NMUR+lh5+XeOzenl65lWYd/nBZXLBbpUMf1AOfbz65xluwCxr2D2lj46iApSIpvE63i3LzFkbGl9GdUiuZJLMFJzOWdhGGc97cB5OVyf8umZLqMHjaImxHEHrnPh1MOVpv87HYJtSBEsN4/omINCMZrk++CRYAIRKRpPKFWV7NQHcvw3m7XLR3KaTYe+0/MINIZwGdou9fLUU3zSd521vDjA/weasH0CyDHq7sZw=="; - - parse_public_key_base64(key).unwrap(); - } - - #[cfg(feature = "rsa")] - #[test] - fn test_nikao() { - env_logger::try_init().unwrap_or(()); - let key = "-----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAw/FG8YLVoXhsUVZcWaY7iZekMxQ2TAfSVh0LTnRuzsumeLhb -0fh4scIt4C4MLwpGe/u3vj290C28jLkOtysqnIpB4iBUrFNRmEz2YuvjOzkFE8Ju -0l1VrTZ9APhpLZvzT2N7YmTXcLz1yWopCe4KqTHczEP4lfkothxEoACXMaxezt5o -wIYfagDaaH6jXJgJk1SQ5VYrROVpDjjX8/Zg01H1faFQUikYx0M8EwL1fY5B80Hd -6DYSok8kUZGfkZT8HQ54DBgocjSs449CVqkVoQC1aDB+LZpMWovY15q7hFgfQmYD -qulbZRWDxxogS6ui/zUR2IpX7wpQMKKkBS1qdQIDAQABAoIBAQCodpcCKfS2gSzP -uapowY1KvP/FkskkEU18EDiaWWyzi1AzVn5LRo+udT6wEacUAoebLU5K2BaMF+aW -Lr1CKnDWaeA/JIDoMDJk+TaU0i5pyppc5LwXTXvOEpzi6rCzL/O++88nR4AbQ7sm -Uom6KdksotwtGvttJe0ktaUi058qaoFZbels5Fwk5bM5GHDdV6De8uQjSfYV813P -tM/6A5rRVBjC5uY0ocBHxPXkqAdHfJuVk0uApjLrbm6k0M2dg1X5oyhDOf7ZIzAg -QGPgvtsVZkQlyrD1OoCMPwzgULPXTe8SktaP9EGvKdMf5kQOqUstqfyx+E4OZa0A -T82weLjBAoGBAOUChhaLQShL3Vsml/Nuhhw5LsxU7Li34QWM6P5AH0HMtsSncH8X -ULYcUKGbCmmMkVb7GtsrHa4ozy0fjq0Iq9cgufolytlvC0t1vKRsOY6poC2MQgaZ -bqRa05IKwhZdHTr9SUwB/ngtVNWRzzbFKLkn2W5oCpQGStAKqz3LbKstAoGBANsJ -EyrXPbWbG+QWzerCIi6shQl+vzOd3cxqWyWJVaZglCXtlyySV2eKWRW7TcVvaXQr -Nzm/99GNnux3pUCY6szy+9eevjFLLHbd+knzCZWKTZiWZWr503h/ztfFwrMzhoAh -z4nukD/OETugPvtG01c2sxZb/F8LH9KORznhlSlpAoGBAJnqg1J9j3JU4tZTbwcG -fo5ThHeCkINp2owPc70GPbvMqf4sBzjz46QyDaM//9SGzFwocplhNhaKiQvrzMnR -LSVucnCEm/xdXLr/y6S6tEiFCwnx3aJv1uQRw2bBYkcDmBTAjVXPdUcyOHU+BYXr -Jv6ioMlKlel8/SUsNoFWypeVAoGAXhr3Bjf1xlm+0O9PRyZjQ0RR4DN5eHbB/XpQ -cL8hclsaK3V5tuek79JL1f9kOYhVeVi74G7uzTSYbCY3dJp+ftGCjDAirNEMaIGU -cEMgAgSqs/0h06VESwg2WRQZQ57GkbR1E2DQzuj9FG4TwSe700OoC9o3gqon4PHJ -/j9CM8kCgYEAtPJf3xaeqtbiVVzpPAGcuPyajTzU0QHPrXEl8zr/+iSK4Thc1K+c -b9sblB+ssEUQD5IQkhTWcsXdslINQeL77WhIMZ2vBAH8Hcin4jgcLmwUZfpfnnFs -QaChXiDsryJZwsRnruvMRX9nedtqHrgnIsJLTXjppIhGhq5Kg4RQfOU= ------END RSA PRIVATE KEY----- -"; - decode_secret_key(key, None).unwrap(); - } - - #[cfg(feature = "rsa")] - #[test] - fn test_decode_pkcs8_rsa_secret_key() { - // Generated using: ssh-keygen -t rsa -b 1024 -m pkcs8 -f $file - let key = "-----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTwWfiCKHw/1F6 -pvm6hZpFSjCVSu4Pp0/M4xT9Cec1+2uj/6uEE9Vh/UhlerkxVbrW/YaqjnlAiemZ -0RGN+sq7b8LxsgvOAo7gdBv13TLkKxNFiRbSy8S257uA9/K7G4Uw+NW22zoLSKCp -pdJOFzaYMIT/UX9EOq9hIIn4bS4nXJ4V5+aHBtMddHHDQPEDHBHuifpP2L4Wopzu -WoQoVtN9cwHSLh0Bd7uT+X9useIJrFzcsxVXwD2WGfR59Ue3rxRu6JqC46Klf55R -5NQ8OQ+7NHXjW5HO076W1GXcnhGKT5CGjglTdk5XxQkNZsz72cHu7RDaADdWAWnE -hSyH7flrAgMBAAECggEAbFdpCjn2eTJ4grOJ1AflTYxO3SOQN8wXxTFuHKUDehgg -E7GNFK99HnyTnPA0bmx5guQGEZ+BpCarsXpJbAYj0dC1wimhZo7igS6G272H+zua -yZoBZmrBQ/++bJbvxxGmjM7TsZHq2bkYEpR3zGKOGUHB2kvdPJB2CNC4JrXdxl7q -djjsr5f/SreDmHqcNBe1LcyWLSsuKTfwTKhsE1qEe6QA2uOpUuFrsdPoeYrfgapu -sK6qnpxvOTJHCN/9jjetrP2fGl78FMBYfXzjAyKSKzLvzOwMAmcHxy50RgUvezx7 -A1RwMpB7VoV0MOpcAjlQ1T7YDH9avdPMzp0EZ24y+QKBgQD/MxDJjHu33w13MnIg -R4BrgXvrgL89Zde5tML2/U9C2LRvFjbBvgnYdqLsuqxDxGY/8XerrAkubi7Fx7QI -m2uvTOZF915UT/64T35zk8nAAFhzicCosVCnBEySvdwaaBKoj/ywemGrwoyprgFe -r8LGSo42uJi0zNf5IxmVzrDlRwKBgQDUa3P/+GxgpUYnmlt63/7sII6HDssdTHa9 -x5uPy8/2ackNR7FruEAJR1jz6akvKnvtbCBeRxLeOFwsseFta8rb2vks7a/3I8ph -gJlbw5Bttpc+QsNgC61TdSKVsfWWae+YT77cfGPM4RaLlxRnccW1/HZjP2AMiDYG -WCiluO+svQKBgQC3a/yk4FQL1EXZZmigysOCgY6Ptfm+J3TmBQYcf/R4F0mYjl7M -4coxyxNPEty92Gulieh5ey0eMhNsFB1SEmNTm/HmV+V0tApgbsJ0T8SyO41Xfar7 -lHZjlLN0xQFt+V9vyA3Wyh9pVGvFiUtywuE7pFqS+hrH2HNindfF1MlQAQKBgQDF -YxBIxKzY5duaA2qMdMcq3lnzEIEXua0BTxGz/n1CCizkZUFtyqnetWjoRrGK/Zxp -FDfDw6G50397nNPQXQEFaaZv5HLGYYC3N8vKJKD6AljqZxmsD03BprA7kEGYwtn8 -m+XMdt46TNMpZXt1YJiLMo1ETmjPXGdvX85tqLs2tQKBgQDCbwd+OBzSiic3IQlD -E/OHAXH6HNHmUL3VD5IiRh4At2VAIl8JsmafUvvbtr5dfT3PA8HB6sDG4iXQsBbR -oTSAo/DtIWt1SllGx6MvcPqL1hp1UWfoIGTnE3unHtgPId+DnjMbTcuZOuGl7evf -abw8VeY2goORjpBXsfydBETbgQ== ------END PRIVATE KEY----- -"; - assert!(decode_secret_key(key, None).unwrap().algorithm().is_rsa()); - test_decode_encode_symmetry(key); - } - - #[test] - fn test_decode_pkcs8_p256_secret_key() { - // Generated using: ssh-keygen -t ecdsa -b 256 -m pkcs8 -f $file - let key = "-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE0C7/pyJDcZTAgWo -ydj6EE8QkZ91jtGoGmdYAVd7LaqhRANCAATWkGOof7R/PAUuOr2+ZPUgB8rGVvgr -qa92U3p4fkJToKXku5eq/32OBj23YMtz76jO3yfMbtG3l1JWLowPA8tV ------END PRIVATE KEY----- -"; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP256 - }, - ); - test_decode_encode_symmetry(key); - } - - #[test] - fn test_decode_pkcs8_p384_secret_key() { - // Generated using: ssh-keygen -t ecdsa -b 384 -m pkcs8 -f $file - let key = "-----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCaqAL30kg+T5BUOYG9 -MrzeDXiUwy9LM8qJGNXiMYou0pVjFZPZT3jAsrUQo47PLQ6hZANiAARuEHbXJBYK -9uyJj4PjT56OHjT2GqMa6i+FTG9vdLtu4OLUkXku+kOuFNjKvEI1JYBrJTpw9kSZ -CI3WfCsQvVjoC7m8qRyxuvR3Rv8gGXR1coQciIoCurLnn9zOFvXCS2Y= ------END PRIVATE KEY----- -"; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP384 - }, - ); - test_decode_encode_symmetry(key); - } - - #[test] - fn test_decode_pkcs8_p521_secret_key() { - // Generated using: ssh-keygen -t ecdsa -b 521 -m pkcs8 -f $file - let key = "-----BEGIN PRIVATE KEY----- -MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB1As9UBUsCiMK7Rzs -EoMgqDM/TK7y7+HgCWzw5UujXvSXCzYCeBgfJszn7dVoJE9G/1ejmpnVTnypdKEu -iIvd4LyhgYkDgYYABAADBCrg7hkomJbCsPMuMcq68ulmo/6Tv8BDS13F8T14v5RN -/0iT/+nwp6CnbBFewMI2TOh/UZNyPpQ8wOFNn9zBmAFCMzkQibnSWK0hrRstY5LT -iaOYDwInbFDsHu8j3TGs29KxyVXMexeV6ROQyXzjVC/quT1R5cOQ7EadE4HvaWhT -Ow== ------END PRIVATE KEY----- -"; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP521 - }, - ); - test_decode_encode_symmetry(key); - } - - #[test] - #[cfg(feature = "legacy-ed25519-pkcs8-parser")] - fn test_decode_pkcs8_ed25519_generated_by_russh_0_43() -> Result<(), crate::keys::Error> { - // Generated by russh 0.43 - let key = "-----BEGIN PRIVATE KEY----- -MHMCAQEwBQYDK2VwBEIEQBHw4cXPpGgA+KdvPF5gxrzML+oa3yQk0JzIbWvmqM5H30RyBF8GrOWz -p77UAd3O4PgYzzFcUc79g8yKtbKhzJGhIwMhAN9EcgRfBqzls6e+1AHdzuD4GM8xXFHO/YPMirWy -ocyR - ------END PRIVATE KEY----- -"; - - assert!(decode_secret_key(key, None)?.algorithm() == ssh_key::Algorithm::Ed25519,); - - let k = decode_secret_key(key, None)?; - let inner = k.key_data().ed25519().unwrap(); - - assert_eq!( - &inner.private.to_bytes(), - &[ - 17, 240, 225, 197, 207, 164, 104, 0, 248, 167, 111, 60, 94, 96, 198, 188, 204, 47, - 234, 26, 223, 36, 36, 208, 156, 200, 109, 107, 230, 168, 206, 71 - ] - ); - - Ok(()) - } - - fn test_decode_encode_symmetry(key: &str) { - let original_key_bytes = data_encoding::BASE64_MIME - .decode( - key.lines() - .filter(|line| !line.starts_with("-----")) - .collect::>() - .join("") - .as_bytes(), - ) - .unwrap(); - let decoded_key = decode_secret_key(key, None).unwrap(); - let encoded_key_bytes = pkcs8::encode_pkcs8(&decoded_key).unwrap(); - assert_eq!(original_key_bytes, encoded_key_bytes); - } - - #[cfg(feature = "rsa")] - #[test] - fn test_o01eg() { - env_logger::try_init().unwrap_or(()); - - let key = "-----BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-128-CBC,EA77308AAF46981303D8C44D548D097E - -QR18hXmAgGehm1QMMYGF34PAtBpTj+8/ZPFx2zZxir7pzDpfYoNAIf/fzLsW1ruG -0xo/ZK/T3/TpMgjmLsCR6q+KU4jmCcCqWQIGWYJt9ljFI5y/CXr5uqP3DKcqtdxQ -fbBAfXJ8ITF+Tj0Cljm2S1KYHor+mkil5Lf/ZNiHxcLfoI3xRnpd+2cemN9Ly9eY -HNTbeWbLosfjwdfPJNWFNV5flm/j49klx/UhXhr5HNFNgp/MlTrvkH4rBt4wYPpE -cZBykt4Fo1KGl95pT22inGxQEXVHF1Cfzrf5doYWxjiRTmfhpPSz/Tt0ev3+jIb8 -Htx6N8tNBoVxwCiQb7jj3XNim2OGohIp5vgW9sh6RDfIvr1jphVOgCTFKSo37xk0 -156EoCVo3VcLf+p0/QitbUHR+RGW/PvUJV/wFR5ShYqjI+N2iPhkD24kftJ/MjPt -AAwCm/GYoYjGDhIzQMB+FETZKU5kz23MQtZFbYjzkcI/RE87c4fkToekNCdQrsoZ -wG0Ne2CxrwwEnipHCqT4qY+lZB9EbqQgbWOXJgxA7lfznBFjdSX7uDc/mnIt9Y6B -MZRXH3PTfotHlHMe+Ypt5lfPBi/nruOl5wLo3L4kY5pUyqR0cXKNycIJZb/pJAnE -ryIb59pZP7njvoHzRqnC9dycnTFW3geK5LU+4+JMUS32F636aorunRCl6IBmVQHL -uZ+ue714fn/Sn6H4dw6IH1HMDG1hr8ozP4sNUCiAQ05LsjDMGTdrUsr2iBBpkQhu -VhUDZy9g/5XF1EgiMbZahmqi5WaJ5K75ToINHb7RjOE7MEiuZ+RPpmYLE0HXyn9X -HTx0ZGr022dDI6nkvUm6OvEwLUUmmGKRHKe0y1EdICGNV+HWqnlhGDbLWeMyUcIY -M6Zh9Dw3WXD3kROf5MrJ6n9MDIXx9jy7nmBh7m6zKjBVIw94TE0dsRcWb0O1IoqS -zLQ6ihno+KsQHDyMVLEUz1TuE52rIpBmqexDm3PdDfCgsNdBKP6QSTcoqcfHKeex -K93FWgSlvFFQQAkJumJJ+B7ZWnK+2pdjdtWwTpflAKNqc8t//WmjWZzCtbhTHCXV -1dnMk7azWltBAuXnjW+OqmuAzyh3ayKgqfW66mzSuyQNa1KqFhqpJxOG7IHvxVfQ -kYeSpqODnL87Zd/dU8s0lOxz3/ymtjPMHlOZ/nHNqW90IIeUwWJKJ46Kv6zXqM1t -MeD1lvysBbU9rmcUdop0D3MOgGpKkinR5gy4pUsARBiz4WhIm8muZFIObWes/GDS -zmmkQRO1IcfXKAHbq/OdwbLBm4vM9nk8vPfszoEQCnfOSd7aWrLRjDR+q2RnzNzh -K+fodaJ864JFIfB/A+aVviVWvBSt0eEbEawhTmNPerMrAQ8tRRhmNxqlDP4gOczi -iKUmK5recsXk5us5Ik7peIR/f9GAghpoJkF0HrHio47SfABuK30pzcj62uNWGljS -3d9UQLCepT6RiPFhks/lgimbtSoiJHql1H9Q/3q4MuO2PuG7FXzlTnui3zGw/Vvy -br8gXU8KyiY9sZVbmplRPF+ar462zcI2kt0a18mr0vbrdqp2eMjb37QDbVBJ+rPE ------END RSA PRIVATE KEY----- -"; - decode_secret_key(key, Some("12345")).unwrap(); - } - - #[cfg(feature = "rsa")] - pub const PKCS8_RSA: &str = "-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAwBGetHjW+3bDQpVktdemnk7JXgu1NBWUM+ysifYLDBvJ9ttX -GNZSyQKA4v/dNr0FhAJ8I9BuOTjYCy1YfKylhl5D/DiSSXFPsQzERMmGgAlYvU2U -+FTxpBC11EZg69CPVMKKevfoUD+PZA5zB7Hc1dXFfwqFc5249SdbAwD39VTbrOUI -WECvWZs6/ucQxHHXP2O9qxWqhzb/ddOnqsDHUNoeceiNiCf2anNymovrIMjAqq1R -t2UP3f06/Zt7Jx5AxKqS4seFkaDlMAK8JkEDuMDOdKI36raHkKanfx8CnGMSNjFQ -QtvnpD8VSGkDTJN3Qs14vj2wvS477BQXkBKN1QIDAQABAoIBABb6xLMw9f+2ENyJ -hTggagXsxTjkS7TElCu2OFp1PpMfTAWl7oDBO7xi+UqvdCcVbHCD35hlWpqsC2Ui -8sBP46n040ts9UumK/Ox5FWaiuYMuDpF6vnfJ94KRcb0+KmeFVf9wpW9zWS0hhJh -jC+yfwpyfiOZ/ad8imGCaOguGHyYiiwbRf381T/1FlaOGSae88h+O8SKTG1Oahq4 -0HZ/KBQf9pij0mfVQhYBzsNu2JsHNx9+DwJkrXT7K9SHBpiBAKisTTCnQmS89GtE -6J2+bq96WgugiM7X6OPnmBmE/q1TgV18OhT+rlvvNi5/n8Z1ag5Xlg1Rtq/bxByP -CeIVHsECgYEA9dX+LQdv/Mg/VGIos2LbpJUhJDj0XWnTRq9Kk2tVzr+9aL5VikEb -09UPIEa2ToL6LjlkDOnyqIMd/WY1W0+9Zf1ttg43S/6Rvv1W8YQde0Nc7QTcuZ1K -9jSSP9hzsa3KZtx0fCtvVHm+ac9fP6u80tqumbiD2F0cnCZcSxOb4+UCgYEAyAKJ -70nNKegH4rTCStAqR7WGAsdPE3hBsC814jguplCpb4TwID+U78Xxu0DQF8WtVJ10 -SJuR0R2q4L9uYWpo0MxdawSK5s9Am27MtJL0mkFQX0QiM7hSZ3oqimsdUdXwxCGg -oktxCUUHDIPJNVd4Xjg0JTh4UZT6WK9hl1zLQzECgYEAiZRCFGc2KCzVLF9m0cXA -kGIZUxFAyMqBv+w3+zq1oegyk1z5uE7pyOpS9cg9HME2TAo4UPXYpLAEZ5z8vWZp -45sp/BoGnlQQsudK8gzzBtnTNp5i/MnnetQ/CNYVIVnWjSxRUHBqdMdRZhv0/Uga -e5KA5myZ9MtfSJA7VJTbyHUCgYBCcS13M1IXaMAt3JRqm+pftfqVs7YeJqXTrGs/ -AiDlGQigRk4quFR2rpAV/3rhWsawxDmb4So4iJ16Wb2GWP4G1sz1vyWRdSnmOJGC -LwtYrvfPHegqvEGLpHa7UsgDpol77hvZriwXwzmLO8A8mxkeW5dfAfpeR5o+mcxW -pvnTEQKBgQCKx6Ln0ku6jDyuDzA9xV2/PET5D75X61R2yhdxi8zurY/5Qon3OWzk -jn/nHT3AZghGngOnzyv9wPMKt9BTHyTB6DlB6bRVLDkmNqZh5Wi8U1/IjyNYI0t2 -xV/JrzLAwPoKk3bkqys3bUmgo6DxVC/6RmMwPQ0rmpw78kOgEej90g== ------END RSA PRIVATE KEY----- -"; - - #[cfg(feature = "rsa")] - #[test] - fn test_pkcs8() { - env_logger::try_init().unwrap_or(()); - println!("test"); - decode_secret_key(PKCS8_RSA, Some("blabla")).unwrap(); - } - - #[cfg(feature = "rsa")] - const PKCS8_ENCRYPTED: &str = "-----BEGIN ENCRYPTED PRIVATE KEY----- -MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQITo1O0b8YrS0CAggA -MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBtLH4T1KOfo1GGr7salhR8BIIE -0KN9ednYwcTGSX3hg7fROhTw7JAJ1D4IdT1fsoGeNu2BFuIgF3cthGHe6S5zceI2 -MpkfwvHbsOlDFWMUIAb/VY8/iYxhNmd5J6NStMYRC9NC0fVzOmrJqE1wITqxtORx -IkzqkgFUbaaiFFQPepsh5CvQfAgGEWV329SsTOKIgyTj97RxfZIKA+TR5J5g2dJY -j346SvHhSxJ4Jc0asccgMb0HGh9UUDzDSql0OIdbnZW5KzYJPOx+aDqnpbz7UzY/ -P8N0w/pEiGmkdkNyvGsdttcjFpOWlLnLDhtLx8dDwi/sbEYHtpMzsYC9jPn3hnds -TcotqjoSZ31O6rJD4z18FOQb4iZs3MohwEdDd9XKblTfYKM62aQJWH6cVQcg+1C7 -jX9l2wmyK26Tkkl5Qg/qSfzrCveke5muZgZkFwL0GCcgPJ8RixSB4GOdSMa/hAMU -kvFAtoV2GluIgmSe1pG5cNMhurxM1dPPf4WnD+9hkFFSsMkTAuxDZIdDk3FA8zof -Yhv0ZTfvT6V+vgH3Hv7Tqcxomy5Qr3tj5vvAqqDU6k7fC4FvkxDh2mG5ovWvc4Nb -Xv8sed0LGpYitIOMldu6650LoZAqJVv5N4cAA2Edqldf7S2Iz1QnA/usXkQd4tLa -Z80+sDNv9eCVkfaJ6kOVLk/ghLdXWJYRLenfQZtVUXrPkaPpNXgD0dlaTN8KuvML -Uw/UGa+4ybnPsdVflI0YkJKbxouhp4iB4S5ACAwqHVmsH5GRnujf10qLoS7RjDAl -o/wSHxdT9BECp7TT8ID65u2mlJvH13iJbktPczGXt07nBiBse6OxsClfBtHkRLzE -QF6UMEXsJnIIMRfrZQnduC8FUOkfPOSXc8r9SeZ3GhfbV/DmWZvFPCpjzKYPsM5+ -N8Bw/iZ7NIH4xzNOgwdp5BzjH9hRtCt4sUKVVlWfEDtTnkHNOusQGKu7HkBF87YZ -RN/Nd3gvHob668JOcGchcOzcsqsgzhGMD8+G9T9oZkFCYtwUXQU2XjMN0R4VtQgZ -rAxWyQau9xXMGyDC67gQ5xSn+oqMK0HmoW8jh2LG/cUowHFAkUxdzGadnjGhMOI2 -zwNJPIjF93eDF/+zW5E1l0iGdiYyHkJbWSvcCuvTwma9FIDB45vOh5mSR+YjjSM5 -nq3THSWNi7Cxqz12Q1+i9pz92T2myYKBBtu1WDh+2KOn5DUkfEadY5SsIu/Rb7ub -5FBihk2RN3y/iZk+36I69HgGg1OElYjps3D+A9AjVby10zxxLAz8U28YqJZm4wA/ -T0HLxBiVw+rsHmLP79KvsT2+b4Diqih+VTXouPWC/W+lELYKSlqnJCat77IxgM9e -YIhzD47OgWl33GJ/R10+RDoDvY4koYE+V5NLglEhbwjloo9Ryv5ywBJNS7mfXMsK -/uf+l2AscZTZ1mhtL38efTQCIRjyFHc3V31DI0UdETADi+/Omz+bXu0D5VvX+7c6 -b1iVZKpJw8KUjzeUV8yOZhvGu3LrQbhkTPVYL555iP1KN0Eya88ra+FUKMwLgjYr -JkUx4iad4dTsGPodwEP/Y9oX/Qk3ZQr+REZ8lg6IBoKKqqrQeBJ9gkm1jfKE6Xkc -Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux ------END ENCRYPTED PRIVATE KEY-----"; - - #[test] - fn test_gpg() { - env_logger::try_init().unwrap_or(()); - let key = [ - 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 129, 0, 163, - 72, 59, 242, 4, 248, 139, 217, 57, 126, 18, 195, 170, 3, 94, 154, 9, 150, 89, 171, 236, - 192, 178, 185, 149, 73, 210, 121, 95, 126, 225, 209, 199, 208, 89, 130, 175, 229, 163, - 102, 176, 155, 69, 199, 155, 71, 214, 170, 61, 202, 2, 207, 66, 198, 147, 65, 10, 176, - 20, 105, 197, 133, 101, 126, 193, 252, 245, 254, 182, 14, 250, 118, 113, 18, 220, 38, - 220, 75, 247, 50, 163, 39, 2, 61, 62, 28, 79, 199, 238, 189, 33, 194, 190, 22, 87, 91, - 1, 215, 115, 99, 138, 124, 197, 127, 237, 228, 170, 42, 25, 117, 1, 106, 36, 54, 163, - 163, 207, 129, 133, 133, 28, 185, 170, 217, 12, 37, 113, 181, 182, 180, 178, 23, 198, - 233, 31, 214, 226, 114, 146, 74, 205, 177, 82, 232, 238, 165, 44, 5, 250, 150, 236, 45, - 30, 189, 254, 118, 55, 154, 21, 20, 184, 235, 223, 5, 20, 132, 249, 147, 179, 88, 146, - 6, 100, 229, 200, 221, 157, 135, 203, 57, 204, 43, 27, 58, 85, 54, 219, 138, 18, 37, - 80, 106, 182, 95, 124, 140, 90, 29, 48, 193, 112, 19, 53, 84, 201, 153, 52, 249, 15, - 41, 5, 11, 147, 18, 8, 27, 31, 114, 45, 224, 118, 111, 176, 86, 88, 23, 150, 184, 252, - 128, 52, 228, 90, 30, 34, 135, 234, 123, 28, 239, 90, 202, 239, 188, 175, 8, 141, 80, - 59, 194, 80, 43, 205, 34, 137, 45, 140, 244, 181, 182, 229, 247, 94, 216, 115, 173, - 107, 184, 170, 102, 78, 249, 4, 186, 234, 169, 148, 98, 128, 33, 115, 232, 126, 84, 76, - 222, 145, 90, 58, 1, 4, 163, 243, 93, 215, 154, 205, 152, 178, 109, 241, 197, 82, 148, - 222, 78, 44, 193, 248, 212, 157, 118, 217, 75, 211, 23, 229, 121, 28, 180, 208, 173, - 204, 14, 111, 226, 25, 163, 220, 95, 78, 175, 189, 168, 67, 159, 179, 176, 200, 150, - 202, 248, 174, 109, 25, 89, 176, 220, 226, 208, 187, 84, 169, 157, 14, 88, 217, 221, - 117, 254, 51, 45, 93, 184, 80, 225, 158, 29, 76, 38, 69, 72, 71, 76, 50, 191, 210, 95, - 152, 175, 26, 207, 91, 7, - ]; - ssh_key::PublicKey::decode(&key).unwrap(); - } - - #[cfg(feature = "rsa")] - #[test] - fn test_pkcs8_encrypted() { - env_logger::try_init().unwrap_or(()); - println!("test"); - decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); - } - - #[cfg(unix)] - async fn test_client_agent(key: PrivateKey) -> Result<(), Box> { - env_logger::try_init().unwrap_or(()); - use std::process::Stdio; - - let dir = tempfile::tempdir()?; - let agent_path = dir.path().join("agent"); - let mut agent = tokio::process::Command::new("ssh-agent") - .arg("-a") - .arg(&agent_path) - .arg("-D") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - // Wait for the socket to be created - while agent_path.canonicalize().is_err() { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - - let public = key.public_key(); - let stream = tokio::net::UnixStream::connect(&agent_path).await?; - let mut client = agent::client::AgentClient::connect(stream); - client.add_identity(&key, &[]).await?; - client.request_identities().await?; - let buf = b"blabla".to_vec(); - let len = buf.len(); - let buf = client - .sign_request( - &AgentIdentity::from(public.clone()), - Some(HashAlg::Sha256), - buf, - ) - .await - .unwrap(); - let (a, b) = buf.split_at(len); - - match key.public_key().key_data() { - ssh_key::public::KeyData::Ed25519 { .. } => { - let sig = &b[b.len() - 64..]; - let sig = ssh_key::Signature::new(key.algorithm(), sig)?; - use signature::Verifier; - assert!(Verifier::verify(public, a, &sig).is_ok()); - } - ssh_key::public::KeyData::Ecdsa { .. } => {} - _ => {} - } - - agent.kill().await?; - agent.wait().await?; - - Ok(()) - } - - #[tokio::test] - #[cfg(unix)] - async fn test_client_agent_ed25519() { - let key = decode_secret_key(ED25519_KEY, Some("blabla")).unwrap(); - test_client_agent(key).await.expect("ssh-agent test failed") - } - - #[tokio::test] - #[cfg(all(unix, feature = "rsa"))] - async fn test_client_agent_rsa() { - let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); - test_client_agent(key).await.expect("ssh-agent test failed") - } - - #[tokio::test] - #[cfg(all(unix, feature = "rsa"))] - async fn test_client_agent_openssh_rsa() { - let key = decode_secret_key(RSA_KEY, None).unwrap(); - test_client_agent(key).await.expect("ssh-agent test failed") - } - - #[test] - #[cfg(all(unix, feature = "rsa"))] - fn test_agent() { - env_logger::try_init().unwrap_or(()); - let dir = tempfile::tempdir().unwrap(); - let agent_path = dir.path().join("agent"); - - let core = tokio::runtime::Runtime::new().unwrap(); - use agent; - use signature::Verifier; - - #[derive(Clone)] - struct X {} - impl agent::server::Agent for X { - fn confirm( - self, - _: std::sync::Arc, - ) -> Box + Send + Unpin> { - Box::new(futures::future::ready((self, true))) - } - } - let agent_path_ = agent_path.clone(); - let (tx, rx) = tokio::sync::oneshot::channel(); - core.spawn(async move { - let mut listener = tokio::net::UnixListener::bind(&agent_path_).unwrap(); - let _ = tx.send(()); - agent::server::serve( - Incoming { - listener: &mut listener, - }, - X {}, - ) - .await - }); - - let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); - core.block_on(async move { - let public = key.public_key(); - // make sure the listener created the file handle - rx.await.unwrap(); - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = agent::client::AgentClient::connect(stream); - client - .add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]) - .await - .unwrap(); - client.request_identities().await.unwrap(); - let buf = b"blabla".to_vec(); - let len = buf.len(); - let buf = client - .sign_request(&AgentIdentity::from(public.clone()), None, buf) - .await - .unwrap(); - let (a, b) = buf.split_at(len); - if let ssh_key::public::KeyData::Ed25519 { .. } = public.key_data() { - let sig = &b[b.len() - 64..]; - let sig = ssh_key::Signature::new(key.algorithm(), sig).unwrap(); - assert!(Verifier::verify(public, a, &sig).is_ok()); - } - }) - } - - #[cfg(unix)] - struct Incoming<'a> { - listener: &'a mut tokio::net::UnixListener, - } - - #[cfg(unix)] - impl futures::stream::Stream for Incoming<'_> { - type Item = Result; - - fn poll_next( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - let (sock, _addr) = futures::ready!(self.get_mut().listener.poll_accept(cx))?; - std::task::Poll::Ready(Some(Ok(sock))) - } - } - - /// Helper to spawn an ssh-agent and return the agent process and socket path - #[cfg(unix)] - async fn spawn_agent() -> Result< - (tokio::process::Child, std::path::PathBuf, tempfile::TempDir), - Box, - > { - use std::process::Stdio; - - let dir = tempfile::tempdir()?; - let agent_path = dir.path().join("agent"); - let agent = tokio::process::Command::new("ssh-agent") - .arg("-a") - .arg(&agent_path) - .arg("-D") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - // Wait for the socket to be created - while agent_path.canonicalize().is_err() { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - - Ok((agent, agent_path, dir)) - } - - /// Helper to create a test certificate - #[cfg(unix)] - fn create_test_cert(ca_key: &PrivateKey, user_key: &PrivateKey) -> ssh_key::Certificate { - use ssh_key::certificate; - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let valid_after = now - 3600; // 1 hour ago - let valid_before = now + 86400 * 365; // 1 year from now - - let mut builder = certificate::Builder::new_with_random_nonce( - &mut rand::rng(), - user_key.public_key(), - valid_after, - valid_before, - ) - .unwrap(); - - builder.serial(1).unwrap(); - builder.key_id("test-cert").unwrap(); - builder.cert_type(certificate::CertType::User).unwrap(); - builder.valid_principal("testuser").unwrap(); - builder.sign(ca_key).unwrap() - } - - #[tokio::test] - #[cfg(unix)] - async fn test_request_identities_full_with_keys_and_certs() { - use crate::keys::agent::{AgentIdentity, client::AgentClient}; - use std::io::Write; - use std::process::Stdio; - - env_logger::try_init().unwrap_or(()); - - let (mut agent, agent_path, dir) = spawn_agent().await.unwrap(); - - // Create a CA key and user key - let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let plain_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Create a certificate - let cert = create_test_cert(&ca_key, &user_key); - - // Write the keys and certificate to temp files - let user_key_path = dir.path().join("user_key"); - let cert_path = dir.path().join("user_key-cert.pub"); - let plain_key_path = dir.path().join("plain_key"); - - // Write user key (the one to be certified) - let mut f = std::fs::File::create(&user_key_path).unwrap(); - f.write_all( - user_key - .to_openssh(ssh_key::LineEnding::LF) - .unwrap() - .as_bytes(), - ) - .unwrap(); - std::fs::set_permissions( - &user_key_path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .unwrap(); - - // Write certificate - let mut f = std::fs::File::create(&cert_path).unwrap(); - f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap(); - - // Write plain key - let mut f = std::fs::File::create(&plain_key_path).unwrap(); - f.write_all( - plain_key - .to_openssh(ssh_key::LineEnding::LF) - .unwrap() - .as_bytes(), - ) - .unwrap(); - std::fs::set_permissions( - &plain_key_path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .unwrap(); - - // Use ssh-add to add the certificate (it will pick up user_key-cert.pub automatically) - let status = tokio::process::Command::new("ssh-add") - .arg(&user_key_path) - .env("SSH_AUTH_SOCK", &agent_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .unwrap(); - assert!(status.success(), "ssh-add for certificate failed"); - - // Add plain key - let status = tokio::process::Command::new("ssh-add") - .arg(&plain_key_path) - .env("SSH_AUTH_SOCK", &agent_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .unwrap(); - assert!(status.success(), "ssh-add for plain key failed"); - - // Connect to agent and test request_identities_full - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = AgentClient::connect(stream); - - let identities = client.request_identities().await.unwrap(); - - // ssh-add with a certificate adds the cert identity, and the plain key adds another - // The exact count depends on ssh-agent behavior - just verify we have both types - assert!(!identities.is_empty(), "Expected at least one identity"); - - // Count the types - let mut key_count = 0; - let mut cert_count = 0; - - for identity in &identities { - match identity { - AgentIdentity::PublicKey { .. } => key_count += 1, - AgentIdentity::Certificate { certificate: c, .. } => { - cert_count += 1; - // Verify the certificate matches what we created - assert_eq!(c.key_id(), "test-cert"); - // Verify public_key() works - let pk = identity.public_key(); - assert_eq!(pk.key_data(), c.public_key()); - } - } - // Verify comment() works - let _ = identity.comment(); - } - - // We should have at least one of each (ssh-add may add both key and cert for the same identity) - assert!( - key_count >= 1, - "Expected at least 1 public key, got {}", - key_count - ); - assert!( - cert_count >= 1, - "Expected at least 1 certificate, got {}", - cert_count - ); - - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } - - #[tokio::test] - #[cfg(unix)] - async fn test_sign_request_cert() { - use crate::keys::agent::client::AgentClient; - use std::io::Write; - use std::process::Stdio; - - env_logger::try_init().unwrap_or(()); - - let (mut agent, agent_path, dir) = spawn_agent().await.unwrap(); - - // Create a CA key and user key - let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Create a certificate - let cert = create_test_cert(&ca_key, &user_key); - - // Write the key and certificate to temp files - let user_key_path = dir.path().join("user_key"); - let cert_path = dir.path().join("user_key-cert.pub"); - - let mut f = std::fs::File::create(&user_key_path).unwrap(); - f.write_all( - user_key - .to_openssh(ssh_key::LineEnding::LF) - .unwrap() - .as_bytes(), - ) - .unwrap(); - std::fs::set_permissions( - &user_key_path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .unwrap(); - - let mut f = std::fs::File::create(&cert_path).unwrap(); - f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap(); - - // Use ssh-add to add the certificate - let status = tokio::process::Command::new("ssh-add") - .arg(&user_key_path) - .env("SSH_AUTH_SOCK", &agent_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .unwrap(); - assert!(status.success(), "ssh-add failed"); - - // Connect to agent and test sign_request_cert - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = AgentClient::connect(stream); - - // Create data to sign - let data_to_sign = b"test data to sign"; - let buf = data_to_sign.to_vec(); - let len = buf.len(); - - // Sign using the certificate (None for hash_alg since Ed25519 doesn't need it) - let signed_buf = client.sign_request(&cert.into(), None, buf).await.unwrap(); - - // Verify the signature is appended to the original data - assert!(signed_buf.len() > len, "Signed buffer should be larger"); - - // Extract and verify signature - let (original, sig_data) = signed_buf.split_at(len); - assert_eq!(original, data_to_sign); - - // The signature should be valid - // For ed25519, signature is 64 bytes, but encoded with type prefix - assert!(sig_data.len() > 64, "Signature data should include type"); - - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } - - #[tokio::test] - #[cfg(unix)] - async fn test_sign_request_cert_missing_key_returns_agent_failure() { - use crate::keys::agent::client::AgentClient; - - env_logger::try_init().unwrap_or(()); - - let (mut agent, agent_path, _dir) = spawn_agent().await.unwrap(); - - // Create a CA key and user key, but DON'T add them to the agent - let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Create a certificate - let cert = create_test_cert(&ca_key, &user_key); - - // Connect to agent WITHOUT adding any keys - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = AgentClient::connect(stream); - - // Verify the agent has no keys - let identities = client.request_identities().await.unwrap(); - assert!(identities.is_empty(), "Agent should have no keys"); - - // Create data to sign - let data_to_sign = b"test data to sign"; - let buf = data_to_sign.to_vec(); - - // Try to sign using the certificate - should fail because the key isn't in the agent - let result = client.sign_request(&cert.into(), None, buf).await; - - // Verify we get an AgentFailure error - assert!( - result.is_err(), - "Signing should fail when key is not in agent" - ); - match result { - Err(Error::AgentFailure) => { - // This is the expected error - } - Err(e) => { - panic!("Expected AgentFailure error, got: {:?}", e); - } - Ok(_) => { - panic!("Expected error, but signing succeeded"); - } - } - - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } - - #[tokio::test] - #[cfg(unix)] - async fn test_sign_request_missing_key_returns_agent_failure() { - use crate::keys::agent::client::AgentClient; - - env_logger::try_init().unwrap_or(()); - - let (mut agent, agent_path, _dir) = spawn_agent().await.unwrap(); - - // Create a key but DON'T add it to the agent - let key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // Connect to agent WITHOUT adding any keys - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = AgentClient::connect(stream); - - // Verify the agent has no keys - let identities = client.request_identities().await.unwrap(); - assert!(identities.is_empty(), "Agent should have no keys"); - - // Create data to sign - let data_to_sign = b"test data to sign"; - let buf = data_to_sign.to_vec(); - - // Try to sign using the public key - should fail because the key isn't in the agent - let result = client - .sign_request(&key.public_key().clone().into(), None, buf) - .await; - - // Verify we get an AgentFailure error - assert!( - result.is_err(), - "Signing should fail when key is not in agent" - ); - match result { - Err(Error::AgentFailure) => { - // This is the expected error - } - Err(e) => { - panic!("Expected AgentFailure error, got: {:?}", e); - } - Ok(_) => { - panic!("Expected error, but signing succeeded"); - } - } - - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } - - /// Helper to create a test RSA certificate - #[cfg(all(unix, feature = "rsa"))] - fn create_test_rsa_cert(ca_key: &PrivateKey, user_key: &PrivateKey) -> ssh_key::Certificate { - use ssh_key::certificate; - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let valid_after = now - 3600; // 1 hour ago - let valid_before = now + 86400 * 365; // 1 year from now - - let mut builder = certificate::Builder::new_with_random_nonce( - &mut rand::rng(), - user_key.public_key(), - valid_after, - valid_before, - ) - .unwrap(); - - builder.serial(1).unwrap(); - builder.key_id("test-rsa-cert").unwrap(); - builder.cert_type(certificate::CertType::User).unwrap(); - builder.valid_principal("testuser").unwrap(); - builder.sign(ca_key).unwrap() - } - - #[tokio::test] - #[cfg(all(unix, feature = "rsa"))] - async fn test_sign_request_cert_rsa() { - use crate::keys::agent::client::AgentClient; - use std::io::Write; - use std::process::Stdio; - - env_logger::try_init().unwrap_or(()); - - let (mut agent, agent_path, dir) = spawn_agent().await.unwrap(); - - // Create RSA CA key and user key - let ca_key = PrivateKey::random( - &mut rand::rng(), - ssh_key::Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }, - ) - .unwrap(); - let user_key = PrivateKey::random( - &mut rand::rng(), - ssh_key::Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }, - ) - .unwrap(); - - // Create a certificate - let cert = create_test_rsa_cert(&ca_key, &user_key); - - // Write the key and certificate to temp files - let user_key_path = dir.path().join("user_rsa_key"); - let cert_path = dir.path().join("user_rsa_key-cert.pub"); - - let mut f = std::fs::File::create(&user_key_path).unwrap(); - f.write_all( - user_key - .to_openssh(ssh_key::LineEnding::LF) - .unwrap() - .as_bytes(), - ) - .unwrap(); - std::fs::set_permissions( - &user_key_path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .unwrap(); - - let mut f = std::fs::File::create(&cert_path).unwrap(); - f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap(); - - // Use ssh-add to add the certificate - let status = tokio::process::Command::new("ssh-add") - .arg(&user_key_path) - .env("SSH_AUTH_SOCK", &agent_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .unwrap(); - assert!(status.success(), "ssh-add failed"); - - // Connect to agent and test sign_request_cert - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = AgentClient::connect(stream); - - // Create data to sign - let data_to_sign = b"test data to sign with RSA cert"; - let buf = data_to_sign.to_vec(); - let len = buf.len(); - - // Sign using the certificate with SHA-256 hash algorithm - let signed_buf = client - .sign_request(&cert.into(), Some(HashAlg::Sha256), buf) - .await - .unwrap(); - - // Verify the signature is appended to the original data - assert!(signed_buf.len() > len, "Signed buffer should be larger"); - - // Extract and verify signature - let (original, sig_data) = signed_buf.split_at(len); - assert_eq!(original, data_to_sign); - - // The RSA signature should be substantial - assert!( - sig_data.len() > 100, - "RSA signature data should be substantial" - ); - - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } - - #[tokio::test] - #[cfg(all(unix, feature = "rsa"))] - async fn test_sign_request_cert_rsa_sha512() { - use crate::keys::agent::client::AgentClient; - use std::io::Write; - use std::process::Stdio; - - env_logger::try_init().unwrap_or(()); - - let (mut agent, agent_path, dir) = spawn_agent().await.unwrap(); - - // Create RSA CA key and user key - let ca_key = PrivateKey::random( - &mut rand::rng(), - ssh_key::Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }, - ) - .unwrap(); - let user_key = PrivateKey::random( - &mut rand::rng(), - ssh_key::Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }, - ) - .unwrap(); - - // Create a certificate - let cert = create_test_rsa_cert(&ca_key, &user_key); - - // Write the key and certificate to temp files - let user_key_path = dir.path().join("user_rsa_key"); - let cert_path = dir.path().join("user_rsa_key-cert.pub"); - - let mut f = std::fs::File::create(&user_key_path).unwrap(); - f.write_all( - user_key - .to_openssh(ssh_key::LineEnding::LF) - .unwrap() - .as_bytes(), - ) - .unwrap(); - std::fs::set_permissions( - &user_key_path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .unwrap(); - - let mut f = std::fs::File::create(&cert_path).unwrap(); - f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap(); - - // Use ssh-add to add the certificate - let status = tokio::process::Command::new("ssh-add") - .arg(&user_key_path) - .env("SSH_AUTH_SOCK", &agent_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .unwrap(); - assert!(status.success(), "ssh-add failed"); - - // Connect to agent and test sign_request_cert with SHA-512 - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut client = AgentClient::connect(stream); - - // Create data to sign - let data_to_sign = b"test data to sign with RSA cert SHA-512"; - let buf = data_to_sign.to_vec(); - let len = buf.len(); - - // Sign using the certificate with SHA-512 hash algorithm - let signed_buf = client - .sign_request(&cert.into(), Some(HashAlg::Sha512), buf) - .await - .unwrap(); - - // Verify the signature is appended to the original data - assert!(signed_buf.len() > len, "Signed buffer should be larger"); - - // Extract and verify signature - let (original, sig_data) = signed_buf.split_at(len); - assert_eq!(original, data_to_sign); - - // The RSA signature should be substantial - assert!( - sig_data.len() > 100, - "RSA signature data should be substantial" - ); - - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } -} diff --git a/crates/bssh-russh/src/lib.rs b/crates/bssh-russh/src/lib.rs deleted file mode 100644 index e7667e5e..00000000 --- a/crates/bssh-russh/src/lib.rs +++ /dev/null @@ -1,96 +0,0 @@ -#![deny( - clippy::unwrap_used, - clippy::expect_used, - clippy::indexing_slicing, - clippy::panic -)] -#![allow(clippy::single_match, clippy::upper_case_acronyms)] -#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)] -// length checked -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Server and client SSH asynchronous library, based on tokio/futures. -//! -//! The normal way to use this library, both for clients and for -//! servers, is by creating *handlers*, i.e. types that implement -//! `client::Handler` for clients and `server::Handler` for -//! servers. -//! -//! * [Writing SSH clients - the `russh::client` module](client) -//! * [Writing SSH servers - the `russh::server` module](server) -//! -//! # Using non-socket IO / writing tunnels -//! -//! The easy way to implement SSH tunnels, like `ProxyCommand` for -//! OpenSSH, is to use the `russh-config` crate, and use the -//! `Stream::tcp_connect` or `Stream::proxy_command` methods of that -//! crate. That crate is a very lightweight layer above Russh, only -//! implementing for external commands the traits used for sockets. -//! -//! # The SSH protocol -//! -//! If we exclude the key exchange and authentication phases, handled -//! by Russh behind the scenes, the rest of the SSH protocol is -//! relatively simple: clients and servers open *channels*, which are -//! just integers used to handle multiple requests in parallel in a -//! single connection. Once a client has obtained a `ChannelId` by -//! calling one of the many `channel_open_…` methods of -//! `client::Connection`, the client may send exec requests and data -//! to the server. -//! -//! A simple client just asking the server to run one command will -//! usually start by calling -//! `client::Connection::channel_open_session`, then -//! `client::Connection::exec`, then possibly -//! `client::Connection::data` a number of times to send data to the -//! command's standard input, and finally `Connection::channel_eof` -//! and `Connection::channel_close`. -//! -//! # Design principles -//! -//! The main goal of this library is conciseness, and reduced size and -//! readability of the library's code. -//! -//! One non-goal is to implement all possible cryptographic algorithms -//! published since the initial release of SSH. Technical debt is -//! easily acquired, and we would need a very strong reason to go -//! against this principle. If you are designing a system from -//! scratch, we urge you to consider recent cryptographic primitives -//! such as Ed25519 for public key cryptography, and Chacha20-Poly1305 -//! for symmetric cryptography and MAC. -//! -//! # Internal details of the event loop -//! -//! It might seem a little odd that the read/write methods for server -//! or client sessions often return neither `Result` nor -//! `Future`. This is because the data sent to the remote side is -//! buffered, because it needs to be encrypted first, and encryption -//! works on buffers, and for many algorithms, not in place. -//! -//! Hence, the event loop keeps waiting for incoming packets, reacts -//! to them by calling the provided `Handler`, which fills some -//! buffers. If the buffers are non-empty, the event loop then sends -//! them to the socket, flushes the socket, empties the buffers and -//! starts again. In the special case of the server, unsolicited -//! messages sent through a `server::Handle` are processed when there -//! is no incoming packet to read. - -#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] -compile_error!( - "`russh` requires enabling either the `ring` or `aws-lc-rs` feature as a crypto backend." -); - -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] -include!("lib_inner.rs"); diff --git a/crates/bssh-russh/src/lib_inner.rs b/crates/bssh-russh/src/lib_inner.rs deleted file mode 100644 index d2ced8f2..00000000 --- a/crates/bssh-russh/src/lib_inner.rs +++ /dev/null @@ -1,503 +0,0 @@ -use std::convert::TryFrom; -use std::fmt::{Debug, Display, Formatter}; -use std::future::{Future, Pending}; - -use futures::future::Either as EitherFuture; -use log::warn; -use parsing::ChannelOpenConfirmation; -pub use russh_cryptovec::CryptoVec; -use ssh_encoding::{Decode, Encode}; -use thiserror::Error; - -#[cfg(test)] -mod tests; - -mod auth; - -mod cert; -/// Cipher names -pub mod cipher; -/// Compression algorithm names -pub mod compression; -/// Key exchange algorithm names -pub mod kex; -/// MAC algorithm names -pub mod mac; - -pub mod keys; - -mod msg; -mod negotiation; -mod ssh_read; -mod sshbuffer; - -pub use negotiation::{Names, Preferred}; - -mod pty; - -pub use pty::Pty; -pub use sshbuffer::SshId; - -mod helpers; - -pub(crate) use helpers::map_err; - -macro_rules! push_packet { - ( $buffer:expr, $x:expr ) => {{ - use byteorder::{BigEndian, ByteOrder}; - let i0 = $buffer.len(); - $buffer.extend(b"\0\0\0\0"); - let x = $x; - let i1 = $buffer.len(); - use std::ops::DerefMut; - let buf = $buffer.deref_mut(); - #[allow(clippy::indexing_slicing)] // length checked - BigEndian::write_u32(&mut buf[i0..], (i1 - i0 - 4) as u32); - x - }}; -} - -mod channels; -pub use channels::{Channel, ChannelMsg, ChannelReadHalf, ChannelStream, ChannelWriteHalf}; - -mod parsing; -mod session; - -/// Server side of this library. -#[cfg(not(target_arch = "wasm32"))] -pub mod server; - -/// Client side of this library. -pub mod client; - -#[derive(Debug)] -pub enum AlgorithmKind { - Kex, - Key, - Cipher, - Compression, - Mac, -} - -#[derive(Debug, Error)] -pub enum Error { - /// The key file could not be parsed. - #[error("Could not read key")] - CouldNotReadKey, - - /// Unspecified problem with the beginning of key exchange. - #[error("Key exchange init failed")] - KexInit, - - /// Unknown algorithm name. - #[error("Unknown algorithm")] - UnknownAlgo, - - /// No common algorithm found during key exchange. - #[error("No common {kind:?} algorithm - ours: {ours:?}, theirs: {theirs:?}")] - NoCommonAlgo { - kind: AlgorithmKind, - ours: Vec, - theirs: Vec, - }, - - /// Invalid SSH version string. - #[error("invalid SSH version string")] - Version, - - /// Error during key exchange. - #[error("Key exchange failed")] - Kex, - - /// Invalid packet authentication code. - #[error("Wrong packet authentication code")] - PacketAuth, - - /// The protocol is in an inconsistent state. - #[error("Inconsistent state of the protocol")] - Inconsistent, - - /// The client is not yet authenticated. - #[error("Not yet authenticated")] - NotAuthenticated, - - /// The client has presented an unsupported authentication method. - #[error("Unsupported authentication method")] - UnsupportedAuthMethod, - - /// Index out of bounds. - #[error("Index out of bounds")] - IndexOutOfBounds, - - /// Unknown server key. - #[error("Unknown server key")] - UnknownKey, - - /// The server provided a wrong signature. - #[error("Wrong server signature")] - WrongServerSig, - - /// Excessive packet size. - #[error("Bad packet size: {0}")] - PacketSize(usize), - - /// Message received/sent on unopened channel. - #[error("Channel not open")] - WrongChannel, - - /// Server refused to open a channel. - #[error("Failed to open channel ({0:?})")] - ChannelOpenFailure(ChannelOpenFailure), - - /// Disconnected - #[error("Disconnected")] - Disconnect, - - /// No home directory found when trying to learn new host key. - #[error("No home directory when saving host key")] - NoHomeDir, - - /// Remote key changed, this could mean a man-in-the-middle attack - /// is being performed on the connection. - #[error("Key changed, line {}", line)] - KeyChanged { line: usize }, - - /// Connection closed by the remote side. - #[error("Connection closed by the remote side")] - HUP, - - /// Connection timeout. - #[error("Connection timeout")] - ConnectionTimeout, - - /// Keepalive timeout. - #[error("Keepalive timeout")] - KeepaliveTimeout, - - /// Inactivity timeout. - #[error("Inactivity timeout")] - InactivityTimeout, - - /// Missing authentication method. - #[error("No authentication method")] - NoAuthMethod, - - #[error("Channel send error")] - SendError, - - #[error("Pending buffer limit reached")] - Pending, - - #[error("Failed to decrypt a packet")] - DecryptionError, - - #[error("The request was rejected by the other party")] - RequestDenied, - - #[error(transparent)] - Keys(#[from] crate::keys::Error), - - #[error(transparent)] - IO(#[from] std::io::Error), - - #[error(transparent)] - Utf8(#[from] std::str::Utf8Error), - - #[error(transparent)] - #[cfg(feature = "flate2")] - Compress(#[from] flate2::CompressError), - - #[error(transparent)] - #[cfg(feature = "flate2")] - Decompress(#[from] flate2::DecompressError), - - #[error(transparent)] - Join(#[from] russh_util::runtime::JoinError), - - #[error(transparent)] - Elapsed(#[from] tokio::time::error::Elapsed), - - #[error("Violation detected during strict key exchange, message {message_type} at seq no {sequence_number}")] - StrictKeyExchangeViolation { - message_type: u8, - sequence_number: usize, - }, - - #[error("Signature: {0}")] - Signature(#[from] signature::Error), - - #[error("SshKey: {0}")] - SshKey(#[from] ssh_key::Error), - - #[error("SshEncoding: {0}")] - SshEncoding(#[from] ssh_encoding::Error), - - #[error("Invalid config: {0}")] - InvalidConfig(String), - - /// This error occurs when the channel is closed and there are no remaining messages in the channel buffer. - /// This is common in SSH-Agent, for example when the Agent client directly rejects an authorization request. - #[error("Unable to receive more messages from the channel")] - RecvError, -} - -pub(crate) fn strict_kex_violation(message_type: u8, sequence_number: usize) -> crate::Error { - warn!( - "strict kex violated at sequence no. {sequence_number:?}, message type: {message_type:?}" - ); - crate::Error::StrictKeyExchangeViolation { - message_type, - sequence_number, - } -} - -#[derive(Debug, Error)] -#[error("Could not reach the event loop")] -pub struct SendError {} - -/// The number of bytes read/written, and the number of seconds before a key -/// re-exchange is requested. -#[derive(Debug, Clone)] -pub struct Limits { - pub rekey_write_limit: usize, - pub rekey_read_limit: usize, - pub rekey_time_limit: std::time::Duration, -} - -impl Limits { - /// Create a new `Limits`, checking that the given bounds cannot lead to - /// nonce reuse. - pub fn new(write_limit: usize, read_limit: usize, time_limit: std::time::Duration) -> Limits { - assert!(write_limit <= 1 << 30 && read_limit <= 1 << 30); - Limits { - rekey_write_limit: write_limit, - rekey_read_limit: read_limit, - rekey_time_limit: time_limit, - } - } -} - -impl Default for Limits { - fn default() -> Self { - // Following the recommendations of - // https://tools.ietf.org/html/rfc4253#section-9 - Limits { - rekey_write_limit: 1 << 30, // 1 Gb - rekey_read_limit: 1 << 30, // 1 Gb - rekey_time_limit: std::time::Duration::from_secs(3600), - } - } -} - -pub use auth::{AgentAuthError, MethodKind, MethodSet, Signer}; - -/// A reason for disconnection. -#[allow(missing_docs)] // This should be relatively self-explanatory. -#[allow(clippy::manual_non_exhaustive)] -#[derive(Debug)] -pub enum Disconnect { - HostNotAllowedToConnect = 1, - ProtocolError = 2, - KeyExchangeFailed = 3, - #[doc(hidden)] - Reserved = 4, - MACError = 5, - CompressionError = 6, - ServiceNotAvailable = 7, - ProtocolVersionNotSupported = 8, - HostKeyNotVerifiable = 9, - ConnectionLost = 10, - ByApplication = 11, - TooManyConnections = 12, - AuthCancelledByUser = 13, - NoMoreAuthMethodsAvailable = 14, - IllegalUserName = 15, -} - -impl TryFrom for Disconnect { - type Error = crate::Error; - - fn try_from(value: u32) -> Result { - Ok(match value { - 1 => Self::HostNotAllowedToConnect, - 2 => Self::ProtocolError, - 3 => Self::KeyExchangeFailed, - 4 => Self::Reserved, - 5 => Self::MACError, - 6 => Self::CompressionError, - 7 => Self::ServiceNotAvailable, - 8 => Self::ProtocolVersionNotSupported, - 9 => Self::HostKeyNotVerifiable, - 10 => Self::ConnectionLost, - 11 => Self::ByApplication, - 12 => Self::TooManyConnections, - 13 => Self::AuthCancelledByUser, - 14 => Self::NoMoreAuthMethodsAvailable, - 15 => Self::IllegalUserName, - _ => return Err(crate::Error::Inconsistent), - }) - } -} - -/// The type of signals that can be sent to a remote process. If you -/// plan to use custom signals, read [the -/// RFC](https://tools.ietf.org/html/rfc4254#section-6.10) to -/// understand the encoding. -#[allow(missing_docs)] -// This should be relatively self-explanatory. -#[derive(Debug, Clone)] -pub enum Sig { - ABRT, - ALRM, - FPE, - HUP, - ILL, - INT, - KILL, - PIPE, - QUIT, - SEGV, - TERM, - USR1, - Custom(String), -} - -impl Sig { - fn name(&self) -> &str { - match *self { - Sig::ABRT => "ABRT", - Sig::ALRM => "ALRM", - Sig::FPE => "FPE", - Sig::HUP => "HUP", - Sig::ILL => "ILL", - Sig::INT => "INT", - Sig::KILL => "KILL", - Sig::PIPE => "PIPE", - Sig::QUIT => "QUIT", - Sig::SEGV => "SEGV", - Sig::TERM => "TERM", - Sig::USR1 => "USR1", - Sig::Custom(ref c) => c, - } - } - fn from_name(name: &str) -> Sig { - match name { - "ABRT" => Sig::ABRT, - "ALRM" => Sig::ALRM, - "FPE" => Sig::FPE, - "HUP" => Sig::HUP, - "ILL" => Sig::ILL, - "INT" => Sig::INT, - "KILL" => Sig::KILL, - "PIPE" => Sig::PIPE, - "QUIT" => Sig::QUIT, - "SEGV" => Sig::SEGV, - "TERM" => Sig::TERM, - "USR1" => Sig::USR1, - x => Sig::Custom(x.to_string()), - } - } -} - -/// Reason for not being able to open a channel. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum ChannelOpenFailure { - AdministrativelyProhibited = 1, - ConnectFailed = 2, - UnknownChannelType = 3, - ResourceShortage = 4, - Unknown = 0, -} - -impl ChannelOpenFailure { - fn from_u32(x: u32) -> Option { - match x { - 1 => Some(ChannelOpenFailure::AdministrativelyProhibited), - 2 => Some(ChannelOpenFailure::ConnectFailed), - 3 => Some(ChannelOpenFailure::UnknownChannelType), - 4 => Some(ChannelOpenFailure::ResourceShortage), - _ => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -/// The identifier of a channel. -pub struct ChannelId(u32); - -impl Decode for ChannelId { - type Error = ssh_encoding::Error; - - fn decode(reader: &mut impl ssh_encoding::Reader) -> Result { - Ok(Self(u32::decode(reader)?)) - } -} - -impl Encode for ChannelId { - fn encoded_len(&self) -> Result { - self.0.encoded_len() - } - - fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error> { - self.0.encode(writer) - } -} - -impl From for u32 { - fn from(c: ChannelId) -> u32 { - c.0 - } -} - -impl Display for ChannelId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -/// The parameters of a channel. -#[derive(Debug)] -pub(crate) struct ChannelParams { - recipient_channel: u32, - sender_channel: ChannelId, - recipient_window_size: u32, - sender_window_size: u32, - recipient_maximum_packet_size: u32, - sender_maximum_packet_size: u32, - /// Has the other side confirmed the channel? - pub confirmed: bool, - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - wants_reply: bool, - /// (buffer, extended stream #, data offset in buffer) - pending_data: std::collections::VecDeque<(bytes::Bytes, Option, usize)>, - pending_eof: bool, - pending_close: bool, -} - -impl ChannelParams { - pub fn confirm(&mut self, c: &ChannelOpenConfirmation) { - self.recipient_channel = c.sender_channel; // "sender" is the sender of the confirmation - self.recipient_window_size = c.initial_window_size; - self.recipient_maximum_packet_size = c.maximum_packet_size; - self.confirmed = true; - } - - pub(crate) fn take_pending_controls(&mut self) -> (bool, bool) { - ( - std::mem::take(&mut self.pending_eof), - std::mem::take(&mut self.pending_close), - ) - } -} - -/// Returns `f(val)` if `val` it is [Some], or a forever pending [Future] if it is [None]. -pub(crate) fn future_or_pending, T>( - val: Option, - f: impl FnOnce(T) -> F, -) -> EitherFuture, F> { - match val { - None => EitherFuture::Left(core::future::pending()), - Some(x) => EitherFuture::Right(f(x)), - } -} diff --git a/crates/bssh-russh/src/mac/crypto.rs b/crates/bssh-russh/src/mac/crypto.rs deleted file mode 100644 index af44add9..00000000 --- a/crates/bssh-russh/src/mac/crypto.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::marker::PhantomData; - -use byteorder::{BigEndian, ByteOrder}; -use digest::typenum::Unsigned; -use digest::{KeyInit, OutputSizeUser}; -use generic_array::{ArrayLength, GenericArray}; -use subtle::ConstantTimeEq; - -use super::{Mac, MacAlgorithm}; - -pub struct CryptoMacAlgorithm< - M: digest::Mac + KeyInit + Send + 'static, - KL: ArrayLength + 'static, ->(pub PhantomData, pub PhantomData); - -pub struct CryptoMac { - pub(crate) key: GenericArray, - pub(crate) p: PhantomData, -} - -impl MacAlgorithm - for CryptoMacAlgorithm -where - ::OutputSize: ArrayLength, -{ - fn key_len(&self) -> usize { - KL::to_usize() - } - - fn make_mac(&self, mac_key: &[u8]) -> Box { - let mut key = GenericArray::::default(); - key.copy_from_slice(mac_key); - Box::new(CryptoMac:: { - key, - p: PhantomData, - }) as Box - } -} - -impl Mac for CryptoMac -where - ::OutputSize: ArrayLength, -{ - fn mac_len(&self) -> usize { - M::OutputSize::to_usize() - } - - fn compute(&self, sequence_number: u32, payload: &[u8], output: &mut [u8]) { - #[allow(clippy::unwrap_used)] - let mut hmac = ::new_from_slice(&self.key).unwrap(); - let mut seqno_buf = [0; 4]; - BigEndian::write_u32(&mut seqno_buf, sequence_number); - hmac.update(&seqno_buf); - hmac.update(payload); - output.copy_from_slice(&hmac.finalize().into_bytes()); - } - - fn verify(&self, sequence_number: u32, payload: &[u8], mac: &[u8]) -> bool { - let mut buf = GenericArray::::default(); - self.compute(sequence_number, payload, &mut buf); - buf.ct_eq(mac).into() - } -} diff --git a/crates/bssh-russh/src/mac/crypto_etm.rs b/crates/bssh-russh/src/mac/crypto_etm.rs deleted file mode 100644 index 7c1f71c8..00000000 --- a/crates/bssh-russh/src/mac/crypto_etm.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::marker::PhantomData; - -use digest::{KeyInit, OutputSizeUser}; -use generic_array::{ArrayLength, GenericArray}; - -use super::crypto::{CryptoMac, CryptoMacAlgorithm}; -use super::{Mac, MacAlgorithm}; - -pub struct CryptoEtmMacAlgorithm< - M: digest::Mac + KeyInit + Send + 'static, - KL: ArrayLength + 'static, ->(pub PhantomData, pub PhantomData); - -impl MacAlgorithm - for CryptoEtmMacAlgorithm -where - ::OutputSize: ArrayLength, -{ - fn key_len(&self) -> usize { - CryptoMacAlgorithm::(self.0, self.1).key_len() - } - - fn make_mac(&self, mac_key: &[u8]) -> Box { - let mut key = GenericArray::::default(); - key.copy_from_slice(mac_key); - Box::new(CryptoEtmMac::(CryptoMac:: { - key, - p: PhantomData, - })) as Box - } -} - -pub struct CryptoEtmMac( - CryptoMac, -); - -impl Mac - for CryptoEtmMac -where - ::OutputSize: ArrayLength, -{ - fn is_etm(&self) -> bool { - true - } - - fn mac_len(&self) -> usize { - self.0.mac_len() - } - - fn compute(&self, sequence_number: u32, payload: &[u8], output: &mut [u8]) { - self.0.compute(sequence_number, payload, output) - } - - fn verify(&self, sequence_number: u32, payload: &[u8], mac: &[u8]) -> bool { - self.0.verify(sequence_number, payload, mac) - } -} diff --git a/crates/bssh-russh/src/mac/mod.rs b/crates/bssh-russh/src/mac/mod.rs deleted file mode 100644 index 67220d1f..00000000 --- a/crates/bssh-russh/src/mac/mod.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -//! -//! This module exports cipher names for use with [Preferred]. -use std::collections::HashMap; -use std::convert::TryFrom; -use std::marker::PhantomData; -use std::sync::LazyLock; - -use delegate::delegate; -use digest::typenum::{U20, U32, U64}; -use hmac::Hmac; -use sha1::Sha1; -use sha2::{Sha256, Sha512}; -use ssh_encoding::Encode; - -use self::crypto::CryptoMacAlgorithm; -use self::crypto_etm::CryptoEtmMacAlgorithm; -use self::none::NoMacAlgorithm; - -mod crypto; -mod crypto_etm; -mod none; - -pub(crate) trait MacAlgorithm { - fn key_len(&self) -> usize; - fn make_mac(&self, key: &[u8]) -> Box; -} - -pub(crate) trait Mac { - fn mac_len(&self) -> usize; - fn is_etm(&self) -> bool { - false - } - fn compute(&self, sequence_number: u32, payload: &[u8], output: &mut [u8]); - fn verify(&self, sequence_number: u32, payload: &[u8], mac: &[u8]) -> bool; -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub struct Name(&'static str); -impl AsRef for Name { - fn as_ref(&self) -> &str { - self.0 - } -} - -impl Encode for Name { - delegate! { to self.as_ref() { - fn encoded_len(&self) -> Result; - fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error>; - }} -} - -impl TryFrom<&str> for Name { - type Error = (); - fn try_from(s: &str) -> Result { - MACS.keys().find(|x| x.0 == s).map(|x| **x).ok_or(()) - } -} - -/// `none` -pub const NONE: Name = Name("none"); -/// `hmac-sha1` -pub const HMAC_SHA1: Name = Name("hmac-sha1"); -/// `hmac-sha2-256` -pub const HMAC_SHA256: Name = Name("hmac-sha2-256"); -/// `hmac-sha2-512` -pub const HMAC_SHA512: Name = Name("hmac-sha2-512"); -/// `hmac-sha1-etm@openssh.com` -pub const HMAC_SHA1_ETM: Name = Name("hmac-sha1-etm@openssh.com"); -/// `hmac-sha2-256-etm@openssh.com` -pub const HMAC_SHA256_ETM: Name = Name("hmac-sha2-256-etm@openssh.com"); -/// `hmac-sha2-512-etm@openssh.com` -pub const HMAC_SHA512_ETM: Name = Name("hmac-sha2-512-etm@openssh.com"); - -pub(crate) static _NONE: NoMacAlgorithm = NoMacAlgorithm {}; -pub(crate) static _HMAC_SHA1: CryptoMacAlgorithm, U20> = - CryptoMacAlgorithm(PhantomData, PhantomData); -pub(crate) static _HMAC_SHA256: CryptoMacAlgorithm, U32> = - CryptoMacAlgorithm(PhantomData, PhantomData); -pub(crate) static _HMAC_SHA512: CryptoMacAlgorithm, U64> = - CryptoMacAlgorithm(PhantomData, PhantomData); -pub(crate) static _HMAC_SHA1_ETM: CryptoEtmMacAlgorithm, U20> = - CryptoEtmMacAlgorithm(PhantomData, PhantomData); -pub(crate) static _HMAC_SHA256_ETM: CryptoEtmMacAlgorithm, U32> = - CryptoEtmMacAlgorithm(PhantomData, PhantomData); -pub(crate) static _HMAC_SHA512_ETM: CryptoEtmMacAlgorithm, U64> = - CryptoEtmMacAlgorithm(PhantomData, PhantomData); - -pub const ALL_MAC_ALGORITHMS: &[&Name] = &[ - &NONE, - &HMAC_SHA1, - &HMAC_SHA256, - &HMAC_SHA512, - &HMAC_SHA1_ETM, - &HMAC_SHA256_ETM, - &HMAC_SHA512_ETM, -]; - -pub(crate) static MACS: LazyLock> = - LazyLock::new(|| { - let mut h: HashMap<&'static Name, &(dyn MacAlgorithm + Send + Sync)> = HashMap::new(); - h.insert(&NONE, &_NONE); - h.insert(&HMAC_SHA1, &_HMAC_SHA1); - h.insert(&HMAC_SHA256, &_HMAC_SHA256); - h.insert(&HMAC_SHA512, &_HMAC_SHA512); - h.insert(&HMAC_SHA1_ETM, &_HMAC_SHA1_ETM); - h.insert(&HMAC_SHA256_ETM, &_HMAC_SHA256_ETM); - h.insert(&HMAC_SHA512_ETM, &_HMAC_SHA512_ETM); - assert_eq!(h.len(), ALL_MAC_ALGORITHMS.len()); - h - }); diff --git a/crates/bssh-russh/src/mac/none.rs b/crates/bssh-russh/src/mac/none.rs deleted file mode 100644 index 82cf5231..00000000 --- a/crates/bssh-russh/src/mac/none.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::{Mac, MacAlgorithm}; - -pub struct NoMacAlgorithm {} - -pub struct NoMac {} - -impl MacAlgorithm for NoMacAlgorithm { - fn key_len(&self) -> usize { - 0 - } - - fn make_mac(&self, _: &[u8]) -> Box { - Box::new(NoMac {}) - } -} - -impl Mac for NoMac { - fn mac_len(&self) -> usize { - 0 - } - - fn compute(&self, _: u32, _: &[u8], _: &mut [u8]) {} - fn verify(&self, _: u32, _: &[u8], _: &[u8]) -> bool { - true - } -} diff --git a/crates/bssh-russh/src/msg.rs b/crates/bssh-russh/src/msg.rs deleted file mode 100644 index 9ad4051c..00000000 --- a/crates/bssh-russh/src/msg.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// https://tools.ietf.org/html/rfc4253#section-12 - -#[cfg(not(target_arch = "wasm32"))] -pub use server::*; - -use crate::{strict_kex_violation, Error}; - -pub const DISCONNECT: u8 = 1; -#[allow(dead_code)] -pub const IGNORE: u8 = 2; -#[allow(dead_code)] -pub const UNIMPLEMENTED: u8 = 3; -#[allow(dead_code)] -pub const DEBUG: u8 = 4; - -pub const SERVICE_REQUEST: u8 = 5; -pub const SERVICE_ACCEPT: u8 = 6; -pub const EXT_INFO: u8 = 7; -pub const KEXINIT: u8 = 20; -pub const NEWKEYS: u8 = 21; - -// http://tools.ietf.org/html/rfc5656#section-7.1 -pub const KEX_ECDH_INIT: u8 = 30; -pub const KEX_ECDH_REPLY: u8 = 31; -pub const KEX_DH_GEX_REQUEST: u8 = 34; -pub const KEX_DH_GEX_GROUP: u8 = 31; -pub const KEX_DH_GEX_INIT: u8 = 32; -pub const KEX_DH_GEX_REPLY: u8 = 33; - -// PQ/T Hybrid Key Exchange with ML-KEM -// https://datatracker.ietf.org/doc/draft-ietf-sshm-mlkem-hybrid-kex/ -pub const KEX_HYBRID_INIT: u8 = 30; -#[allow(dead_code)] -pub const KEX_HYBRID_REPLY: u8 = 31; - -// https://tools.ietf.org/html/rfc4250#section-4.1.2 -pub const USERAUTH_REQUEST: u8 = 50; -pub const USERAUTH_FAILURE: u8 = 51; -pub const USERAUTH_SUCCESS: u8 = 52; -pub const USERAUTH_BANNER: u8 = 53; - -pub const USERAUTH_INFO_RESPONSE: u8 = 61; - -// some numbers have same meaning -pub const USERAUTH_INFO_REQUEST_OR_USERAUTH_PK_OK: u8 = 60; - -// https://tools.ietf.org/html/rfc4254#section-9 -pub const GLOBAL_REQUEST: u8 = 80; -pub const REQUEST_SUCCESS: u8 = 81; -pub const REQUEST_FAILURE: u8 = 82; - -pub const CHANNEL_OPEN: u8 = 90; -pub const CHANNEL_OPEN_CONFIRMATION: u8 = 91; -pub const CHANNEL_OPEN_FAILURE: u8 = 92; -pub const CHANNEL_WINDOW_ADJUST: u8 = 93; -pub const CHANNEL_DATA: u8 = 94; -pub const CHANNEL_EXTENDED_DATA: u8 = 95; -pub const CHANNEL_EOF: u8 = 96; -pub const CHANNEL_CLOSE: u8 = 97; -pub const CHANNEL_REQUEST: u8 = 98; -pub const CHANNEL_SUCCESS: u8 = 99; -pub const CHANNEL_FAILURE: u8 = 100; - -#[allow(dead_code)] -pub const SSH_OPEN_CONNECT_FAILED: u8 = 2; -pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u8 = 3; -#[allow(dead_code)] -pub const SSH_OPEN_RESOURCE_SHORTAGE: u8 = 4; - -#[cfg(not(target_arch = "wasm32"))] -mod server { - // https://tools.ietf.org/html/rfc4256#section-5 - pub const USERAUTH_INFO_REQUEST: u8 = 60; - pub const USERAUTH_PK_OK: u8 = 60; - pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u8 = 1; -} - -/// Validate a message+seqno against a strict kex order pattern -/// Returns: -/// - `Some(true)` if the message is valid at this position -/// - `Some(false)` if the message is invalid at this position -/// - `None` if the `seqno` is not covered by strict kex -fn validate_msg_strict_kex(msg_type: u8, seqno: usize, order: &[u8]) -> Option { - order.get(seqno).map(|expected| expected == &msg_type) -} - -/// Validate a message+seqno against multiple strict kex order patterns -fn validate_msg_strict_kex_alt_order(msg_type: u8, seqno: usize, orders: &[&[u8]]) -> Option { - let mut valid = None; // did not match yet - for order in orders { - let result = validate_msg_strict_kex(msg_type, seqno, order); - valid = match (valid, result) { - // If we matched a valid msg, it's now valid forever - (Some(true), _) | (_, Some(true)) => Some(true), - // If we matched an invalid msg and we didn't find a valid one yet, it's now invalid - (None | Some(false), Some(false)) => Some(false), - // If the message was beyond the current pattern, no change - (x, None) => x, - }; - } - valid -} - -pub(crate) fn validate_client_msg_strict_kex(msg_type: u8, seqno: usize) -> Result<(), Error> { - if Some(false) - == validate_msg_strict_kex_alt_order( - msg_type, - seqno, - &[ - &[KEXINIT, KEX_ECDH_INIT, NEWKEYS], - &[KEXINIT, KEX_DH_GEX_REQUEST, KEX_DH_GEX_INIT, NEWKEYS], - ], - ) - { - return Err(strict_kex_violation(msg_type, seqno)); - } - Ok(()) -} - -pub(crate) fn validate_server_msg_strict_kex(msg_type: u8, seqno: usize) -> Result<(), Error> { - if Some(false) - == validate_msg_strict_kex_alt_order( - msg_type, - seqno, - &[ - &[KEXINIT, KEX_ECDH_REPLY, NEWKEYS], - &[KEXINIT, KEX_DH_GEX_GROUP, KEX_DH_GEX_REPLY, NEWKEYS], - ], - ) - { - return Err(strict_kex_violation(msg_type, seqno)); - } - Ok(()) -} - -const ALL_KEX_MESSAGES: &[u8] = &[ - KEXINIT, - KEX_ECDH_INIT, - KEX_ECDH_REPLY, - KEX_DH_GEX_GROUP, - KEX_DH_GEX_INIT, - KEX_DH_GEX_REPLY, - KEX_DH_GEX_REQUEST, - NEWKEYS, -]; - -pub(crate) fn is_kex_msg(msg: u8) -> bool { - ALL_KEX_MESSAGES.contains(&msg) -} diff --git a/crates/bssh-russh/src/negotiation.rs b/crates/bssh-russh/src/negotiation.rs deleted file mode 100644 index df0ea058..00000000 --- a/crates/bssh-russh/src/negotiation.rs +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -use std::borrow::Cow; - -use bytes::Bytes; -use log::debug; -use rand_core::Rng; -use ssh_encoding::{Decode, Encode}; -use ssh_key::{Algorithm, EcdsaCurve, HashAlg, PrivateKey}; - -use crate::cipher::CIPHERS; -use crate::helpers::NameList; -use crate::kex::{ - EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, KexCause, -}; -use crate::keys::key::safe_rng; -use crate::parsing::ensure_end; -#[cfg(not(target_arch = "wasm32"))] -use crate::server::Config; -use crate::sshbuffer::PacketWriter; -use crate::{AlgorithmKind, Error, cipher, compression, kex, mac, msg}; - -#[cfg(target_arch = "wasm32")] -/// WASM-only stub -pub struct Config { - keys: Vec, -} - -#[derive(Debug, Clone)] -pub struct Names { - pub kex: kex::Name, - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - pub key: Algorithm, - pub cipher: cipher::Name, - pub client_mac: mac::Name, - pub server_mac: mac::Name, - pub server_compression: compression::Compression, - pub client_compression: compression::Compression, - pub ignore_guessed: bool, - // Prevent accidentally contructing [Names] without a [KeyCause] - // as strict kext algo is not sent during a rekey and hence the state - // of [strict_kex] cannot be known without a [KexCause]. - strict_kex: bool, -} - -impl Names { - pub fn strict_kex(&self) -> bool { - self.strict_kex - } -} - -/// Lists of preferred algorithms. This is normally hard-coded into implementations. -#[derive(Debug, Clone)] -pub struct Preferred { - /// Preferred key exchange algorithms. - pub kex: Cow<'static, [kex::Name]>, - /// Preferred host & public key algorithms. - pub key: Cow<'static, [Algorithm]>, - /// Preferred symmetric ciphers. - pub cipher: Cow<'static, [cipher::Name]>, - /// Preferred MAC algorithms. - pub mac: Cow<'static, [mac::Name]>, - /// Preferred compression algorithms. - pub compression: Cow<'static, [compression::Name]>, -} - -pub(crate) fn is_key_compatible_with_algo(key: &PrivateKey, algo: &Algorithm) -> bool { - match algo { - // All RSA keys are compatible with all RSA based algos. - Algorithm::Rsa { .. } => key.algorithm().is_rsa(), - // Other keys have to match exactly - a => key.algorithm() == *a, - } -} - -impl Preferred { - pub(crate) fn possible_host_key_algos_for_keys( - &self, - available_host_keys: &[PrivateKey], - ) -> Vec { - self.key - .iter() - .filter(|n| { - available_host_keys - .iter() - .any(|k| is_key_compatible_with_algo(k, n)) - }) - .cloned() - .collect::>() - } -} - -const SAFE_KEX_ORDER: &[kex::Name] = &[ - kex::MLKEM768X25519_SHA256, - kex::CURVE25519, - kex::CURVE25519_PRE_RFC_8731, - kex::DH_GEX_SHA256, - kex::DH_G18_SHA512, - kex::DH_G17_SHA512, - kex::DH_G16_SHA512, - kex::DH_G15_SHA512, - kex::DH_G14_SHA256, - kex::EXTENSION_SUPPORT_AS_CLIENT, - kex::EXTENSION_SUPPORT_AS_SERVER, - kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, - kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, -]; - -const KEX_EXTENSION_NAMES: &[kex::Name] = &[ - kex::EXTENSION_SUPPORT_AS_CLIENT, - kex::EXTENSION_SUPPORT_AS_SERVER, - kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, - kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, -]; - -const CIPHER_ORDER: &[cipher::Name] = &[ - cipher::CHACHA20_POLY1305, - cipher::AES_256_GCM, - cipher::AES_256_CTR, - cipher::AES_192_CTR, - cipher::AES_128_CTR, -]; - -// SHA-1 MAC variants are excluded from defaults. -const SAFE_HMAC_ORDER: &[mac::Name] = &[ - mac::HMAC_SHA512_ETM, - mac::HMAC_SHA256_ETM, - mac::HMAC_SHA512, - mac::HMAC_SHA256, -]; - -const COMPRESSION_ORDER: &[compression::Name] = &[ - compression::NONE, - #[cfg(feature = "flate2")] - compression::ZLIB, - #[cfg(feature = "flate2")] - compression::ZLIB_LEGACY, -]; - -impl Preferred { - pub const DEFAULT: Preferred = Preferred { - kex: Cow::Borrowed(SAFE_KEX_ORDER), - key: Cow::Borrowed(&[ - Algorithm::Ed25519, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP256, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP384, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP521, - }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }, - Algorithm::Rsa { hash: None }, - ]), - cipher: Cow::Borrowed(CIPHER_ORDER), - mac: Cow::Borrowed(SAFE_HMAC_ORDER), - compression: Cow::Borrowed(COMPRESSION_ORDER), - }; - - pub const COMPRESSED: Preferred = Preferred { - kex: Cow::Borrowed(SAFE_KEX_ORDER), - key: Preferred::DEFAULT.key, - cipher: Cow::Borrowed(CIPHER_ORDER), - mac: Cow::Borrowed(SAFE_HMAC_ORDER), - compression: Cow::Borrowed(COMPRESSION_ORDER), - }; -} - -impl Default for Preferred { - fn default() -> Preferred { - Preferred::DEFAULT - } -} - -pub(crate) trait Select { - fn is_server() -> bool; - - fn select + Clone, B: AsRef + Clone>( - a: &[A], - b: &[B], - kind: AlgorithmKind, - ) -> Result<(bool, A), Error>; - - /// `available_host_keys`, if present, is used to limit the host key algorithms to the ones we have keys for. - fn read_kex( - buffer: &[u8], - pref: &Preferred, - available_host_keys: Option<&[PrivateKey]>, - cause: &KexCause, - ) -> Result { - let &Some(mut r) = &buffer.get(17..) else { - return Err(Error::Inconsistent); - }; - - // Key exchange - - let kex_list = NameList::decode(&mut r)?; - // Filter out extension kex names from both lists before selecting - let _local_kexes_no_ext = pref - .kex - .iter() - .filter(|k| !KEX_EXTENSION_NAMES.contains(k)) - .cloned() - .collect::>(); - let _remote_kexes_no_ext = kex_list - .iter() - .filter(|k| { - kex::Name::try_from(k.as_str()) - .ok() - .map(|k| !KEX_EXTENSION_NAMES.contains(&k)) - .unwrap_or(false) - }) - .collect::>(); - let (kex_both_first, kex_algorithm) = Self::select( - &_local_kexes_no_ext, - &_remote_kexes_no_ext, - AlgorithmKind::Kex, - )?; - - // Strict kex detection - - let strict_kex_requested = pref.kex.contains(if Self::is_server() { - &EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER - } else { - &EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT - }); - let strict_kex_provided = Self::select( - &[if Self::is_server() { - EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT - } else { - EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER - }], - &kex_list, - AlgorithmKind::Kex, - ) - .is_ok(); - - if strict_kex_requested && strict_kex_provided { - debug!("strict kex enabled") - } - - // Host key - - let key_list = NameList::decode(&mut r)?; - let possible_host_key_algos = match available_host_keys { - Some(available_host_keys) => pref.possible_host_key_algos_for_keys(available_host_keys), - None => pref.key.iter().map(ToOwned::to_owned).collect::>(), - }; - - let (key_both_first, key_algorithm) = - Self::select(&possible_host_key_algos[..], &key_list, AlgorithmKind::Key)?; - - // Cipher - - let cipher_list = NameList::decode(&mut r)?; - let (_cipher_both_first, cipher) = - Self::select(&pref.cipher, &cipher_list, AlgorithmKind::Cipher)?; - String::decode(&mut r)?; // cipher server-to-client. - - // MAC - - let need_mac = CIPHERS.get(&cipher).map(|x| x.needs_mac()).unwrap_or(false); - - let client_mac = - match Self::select(&pref.mac, &NameList::decode(&mut r)?, AlgorithmKind::Mac) { - Ok((_, m)) => m, - Err(e) => { - if need_mac { - return Err(e); - } else { - mac::NONE - } - } - }; - let server_mac = - match Self::select(&pref.mac, &NameList::decode(&mut r)?, AlgorithmKind::Mac) { - Ok((_, m)) => m, - Err(e) => { - if need_mac { - return Err(e); - } else { - mac::NONE - } - } - }; - - // Compression - - // client-to-server compression. - let client_compression = compression::Compression::new( - &Self::select( - &pref.compression, - &NameList::decode(&mut r)?, - AlgorithmKind::Compression, - )? - .1, - ); - - // server-to-client compression. - let server_compression = compression::Compression::new( - &Self::select( - &pref.compression, - &NameList::decode(&mut r)?, - AlgorithmKind::Compression, - )? - .1, - ); - String::decode(&mut r)?; // languages client-to-server - String::decode(&mut r)?; // languages server-to-client - - let follows = u8::decode(&mut r)? != 0; - u32::decode(&mut r)?; - ensure_end(&r)?; - Ok(Names { - kex: kex_algorithm, - key: key_algorithm, - cipher, - client_mac, - server_mac, - client_compression, - server_compression, - // Ignore the next packet if (1) it follows and (2) it's not the correct guess. - ignore_guessed: follows && !(kex_both_first && key_both_first), - strict_kex: (strict_kex_requested && strict_kex_provided) || cause.is_strict_rekey(), - }) - } -} - -pub struct Server; -pub struct Client; - -impl Select for Server { - fn is_server() -> bool { - true - } - - fn select + Clone, B: AsRef + Clone>( - server_list: &[A], - client_list: &[B], - kind: AlgorithmKind, - ) -> Result<(bool, A), Error> { - let mut both_first_choice = true; - for c in client_list { - for s in server_list { - if c.as_ref() == s.as_ref() { - return Ok((both_first_choice, s.clone())); - } - both_first_choice = false - } - } - Err(Error::NoCommonAlgo { - kind, - ours: server_list.iter().map(|x| x.as_ref().to_owned()).collect(), - theirs: client_list.iter().map(|x| x.as_ref().to_owned()).collect(), - }) - } -} - -impl Select for Client { - fn is_server() -> bool { - false - } - - fn select + Clone, B: AsRef + Clone>( - client_list: &[A], - server_list: &[B], - kind: AlgorithmKind, - ) -> Result<(bool, A), Error> { - let mut both_first_choice = true; - for c in client_list { - for s in server_list { - if s.as_ref() == c.as_ref() { - return Ok((both_first_choice, c.clone())); - } - both_first_choice = false - } - } - Err(Error::NoCommonAlgo { - kind, - ours: client_list.iter().map(|x| x.as_ref().to_owned()).collect(), - theirs: server_list.iter().map(|x| x.as_ref().to_owned()).collect(), - }) - } -} - -pub(crate) fn write_kex( - prefs: &Preferred, - writer: &mut PacketWriter, - server_config: Option<&Config>, -) -> Result { - writer.packet_bytes(|w| { - msg::KEXINIT.encode(w)?; - - let mut cookie = [0; 16]; - safe_rng().fill_bytes(&mut cookie); - for b in cookie { - b.encode(w)?; - } - - NameList( - prefs - .kex - .iter() - .filter(|k| { - !(if server_config.is_some() { - [ - crate::kex::EXTENSION_SUPPORT_AS_CLIENT, - crate::kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, - ] - } else { - [ - crate::kex::EXTENSION_SUPPORT_AS_SERVER, - crate::kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, - ] - }) - .contains(*k) - }) - .map(|x| x.as_ref().to_owned()) - .collect(), - ) - .encode(w)?; // kex algo - - if let Some(server_config) = server_config { - // Only advertise host key algorithms that we have keys for. - NameList( - prefs - .key - .iter() - .filter(|algo| { - server_config - .keys - .iter() - .any(|k| is_key_compatible_with_algo(k, algo)) - }) - .map(|x| x.to_string()) - .collect(), - ) - .encode(w)?; - } else { - NameList(prefs.key.iter().map(ToString::to_string).collect()).encode(w)?; - } - - // cipher client to server - NameList( - prefs - .cipher - .iter() - .map(|x| x.as_ref().to_string()) - .collect(), - ) - .encode(w)?; - - // cipher server to client - NameList( - prefs - .cipher - .iter() - .map(|x| x.as_ref().to_string()) - .collect(), - ) - .encode(w)?; - - // mac client to server - NameList(prefs.mac.iter().map(|x| x.as_ref().to_string()).collect()).encode(w)?; - - // mac server to client - NameList(prefs.mac.iter().map(|x| x.as_ref().to_string()).collect()).encode(w)?; - - // compress client to server - NameList( - prefs - .compression - .iter() - .map(|x| x.as_ref().to_string()) - .collect(), - ) - .encode(w)?; - - // compress server to client - NameList( - prefs - .compression - .iter() - .map(|x| x.as_ref().to_string()) - .collect(), - ) - .encode(w)?; - - Vec::::new().encode(w)?; // languages client to server - Vec::::new().encode(w)?; // languages server to client - - 0u8.encode(w)?; // doesn't follow - 0u32.encode(w)?; // reserved - Ok(()) - }) -} diff --git a/crates/bssh-russh/src/parsing.rs b/crates/bssh-russh/src/parsing.rs deleted file mode 100644 index 8b7ee867..00000000 --- a/crates/bssh-russh/src/parsing.rs +++ /dev/null @@ -1,272 +0,0 @@ -use ssh_encoding::{Decode, Encode, Reader}; - -use crate::msg; - -use crate::map_err; - -/// Require a decoded known-message payload to be fully consumed. -/// -/// SSH RFCs and implemented OpenSSH extensions define exact field layouts for -/// known message types. Callers use this after decoding those fields so -/// malformed packets with trailing payload bytes are rejected instead of being -/// treated as canonical messages. -pub(crate) fn ensure_end(reader: &impl Reader) -> Result<(), crate::Error> { - if reader.is_finished() { - Ok(()) - } else { - Err(ssh_encoding::Error::TrailingData { - remaining: reader.remaining_len(), - } - .into()) - } -} - -#[derive(Debug)] -pub struct OpenChannelMessage { - pub typ: ChannelType, - pub recipient_channel: u32, - pub recipient_window_size: u32, - pub recipient_maximum_packet_size: u32, -} - -impl OpenChannelMessage { - /// Parse an SSH `CHANNEL_OPEN` payload. - /// - /// Known channel types are parsed according to their fixed layouts and must - /// not contain trailing bytes. Unknown extension channel types remain - /// intentionally opaque so applications can implement extension-specific - /// parsing and compatibility behavior. - pub fn parse(r: &mut R) -> Result { - // https://tools.ietf.org/html/rfc4254#section-5.1 - let typ = map_err!(String::decode(r))?; - let sender = map_err!(u32::decode(r))?; - let window = map_err!(u32::decode(r))?; - let maxpacket = map_err!(u32::decode(r))?; - - let typ = match typ.as_str() { - "session" => { - ensure_end(r)?; - ChannelType::Session - } - "x11" => { - let originator_address = map_err!(String::decode(r))?; - let originator_port = map_err!(u32::decode(r))?; - ensure_end(r)?; - ChannelType::X11 { - originator_address, - originator_port, - } - } - "direct-tcpip" => { - let info = TcpChannelInfo::decode(r)?; - ensure_end(r)?; - ChannelType::DirectTcpip(info) - } - "direct-streamlocal@openssh.com" => { - let info = StreamLocalChannelInfo::decode(r)?; - String::decode(r)?; // originator address/reserved - u32::decode(r)?; // originator port/reserved - ensure_end(r)?; - ChannelType::DirectStreamLocal(info) - } - "forwarded-tcpip" => { - let info = TcpChannelInfo::decode(r)?; - ensure_end(r)?; - ChannelType::ForwardedTcpIp(info) - } - "forwarded-streamlocal@openssh.com" => { - let info = StreamLocalChannelInfo::decode(r)?; - String::decode(r)?; // reserved - ensure_end(r)?; - ChannelType::ForwardedStreamLocal(info) - } - "auth-agent@openssh.com" => { - ensure_end(r)?; - ChannelType::AgentForward - } - _ => ChannelType::Unknown { typ }, - }; - - Ok(Self { - typ, - recipient_channel: sender, - recipient_window_size: window, - recipient_maximum_packet_size: maxpacket, - }) - } - - /// Pushes a confirmation that this channel was opened to the vec. - pub fn confirm( - &self, - buffer: &mut Vec, - sender_channel: u32, - window_size: u32, - packet_size: u32, - ) -> Result<(), crate::Error> { - push_packet!(buffer, { - msg::CHANNEL_OPEN_CONFIRMATION.encode(buffer)?; - self.recipient_channel.encode(buffer)?; // remote channel number. - sender_channel.encode(buffer)?; // our channel number. - window_size.encode(buffer)?; - packet_size.encode(buffer)?; - }); - Ok(()) - } - - /// Pushes a failure message to the vec. - pub fn fail( - &self, - buffer: &mut Vec, - reason: u8, - message: &[u8], - ) -> Result<(), crate::Error> { - push_packet!(buffer, { - msg::CHANNEL_OPEN_FAILURE.encode(buffer)?; - self.recipient_channel.encode(buffer)?; - (reason as u32).encode(buffer)?; - message.encode(buffer)?; - "en".encode(buffer)?; - }); - Ok(()) - } - - /// Pushes an unknown type error to the vec. - pub fn unknown_type(&self, buffer: &mut Vec) -> Result<(), crate::Error> { - self.fail( - buffer, - msg::SSH_OPEN_UNKNOWN_CHANNEL_TYPE, - b"Unknown channel type", - ) - } -} - -#[derive(Debug)] -pub enum ChannelType { - Session, - X11 { - originator_address: String, - originator_port: u32, - }, - DirectTcpip(TcpChannelInfo), - DirectStreamLocal(StreamLocalChannelInfo), - ForwardedTcpIp(TcpChannelInfo), - ForwardedStreamLocal(StreamLocalChannelInfo), - AgentForward, - Unknown { - typ: String, - }, -} - -#[derive(Debug)] -pub struct TcpChannelInfo { - pub host_to_connect: String, - pub port_to_connect: u32, - pub originator_address: String, - pub originator_port: u32, -} - -#[derive(Debug)] -pub struct StreamLocalChannelInfo { - pub socket_path: String, -} - -impl Decode for StreamLocalChannelInfo { - type Error = ssh_encoding::Error; - - fn decode(r: &mut impl Reader) -> Result { - let socket_path = String::decode(r)?.to_owned(); - Ok(Self { socket_path }) - } -} - -impl Decode for TcpChannelInfo { - type Error = ssh_encoding::Error; - - fn decode(r: &mut impl Reader) -> Result { - let host_to_connect = String::decode(r)?; - let port_to_connect = u32::decode(r)?; - let originator_address = String::decode(r)?; - let originator_port = u32::decode(r)?; - - Ok(Self { - host_to_connect, - port_to_connect, - originator_address, - originator_port, - }) - } -} - -#[derive(Debug)] -pub(crate) struct ChannelOpenConfirmation { - pub recipient_channel: u32, - pub sender_channel: u32, - pub initial_window_size: u32, - pub maximum_packet_size: u32, -} - -impl Decode for ChannelOpenConfirmation { - type Error = ssh_encoding::Error; - - fn decode(r: &mut impl Reader) -> Result { - let recipient_channel = u32::decode(r)?; - let sender_channel = u32::decode(r)?; - let initial_window_size = u32::decode(r)?; - let maximum_packet_size = u32::decode(r)?; - - Ok(Self { - recipient_channel, - sender_channel, - initial_window_size, - maximum_packet_size, - }) - } -} - -#[cfg(test)] -mod tests { - use super::{ChannelType, OpenChannelMessage}; - use crate::tests::raw_no_crypto::{channel_open_payload, encode_string, push_u32}; - - #[test] - fn known_channel_open_with_trailing_bytes_is_rejected() { - let mut payload = channel_open_payload(b"session"); - payload.push(0); - - assert!( - OpenChannelMessage::parse(&mut payload.as_slice()).is_err(), - "known channel-open type accepted trailing bytes" - ); - } - - #[test] - fn unknown_channel_open_with_extra_payload_stays_permissive() { - let mut payload = channel_open_payload(b"unknown@example.com"); - payload.extend_from_slice(b"opaque"); - - let parsed = OpenChannelMessage::parse(&mut payload.as_slice()) - .expect("unknown channel-open payload should remain opaque"); - - assert!(matches!(parsed.typ, ChannelType::Unknown { .. })); - } - - #[test] - fn openssh_streamlocal_channel_open_reserved_fields_are_consumed() { - let mut direct = channel_open_payload(b"direct-streamlocal@openssh.com"); - encode_string(&mut direct, b"/tmp/socket"); - encode_string(&mut direct, b""); - push_u32(&mut direct, 0); - - let parsed = OpenChannelMessage::parse(&mut direct.as_slice()) - .expect("direct streamlocal reserved fields should be consumed"); - assert!(matches!(parsed.typ, ChannelType::DirectStreamLocal(_))); - - let mut forwarded = channel_open_payload(b"forwarded-streamlocal@openssh.com"); - encode_string(&mut forwarded, b"/tmp/socket"); - encode_string(&mut forwarded, b""); - - let parsed = OpenChannelMessage::parse(&mut forwarded.as_slice()) - .expect("forwarded streamlocal reserved field should be consumed"); - assert!(matches!(parsed.typ, ChannelType::ForwardedStreamLocal(_))); - } -} diff --git a/crates/bssh-russh/src/pty.rs b/crates/bssh-russh/src/pty.rs deleted file mode 100755 index 6ee8b4ea..00000000 --- a/crates/bssh-russh/src/pty.rs +++ /dev/null @@ -1,134 +0,0 @@ -#[allow(non_camel_case_types, missing_docs)] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -/// Standard pseudo-terminal codes. -pub enum Pty { - TTY_OP_END = 0, - VINTR = 1, - VQUIT = 2, - VERASE = 3, - VKILL = 4, - VEOF = 5, - VEOL = 6, - VEOL2 = 7, - VSTART = 8, - VSTOP = 9, - VSUSP = 10, - VDSUSP = 11, - - VREPRINT = 12, - VWERASE = 13, - VLNEXT = 14, - VFLUSH = 15, - VSWTCH = 16, - VSTATUS = 17, - VDISCARD = 18, - IGNPAR = 30, - PARMRK = 31, - INPCK = 32, - ISTRIP = 33, - INLCR = 34, - IGNCR = 35, - ICRNL = 36, - IUCLC = 37, - IXON = 38, - IXANY = 39, - IXOFF = 40, - IMAXBEL = 41, - IUTF8 = 42, - ISIG = 50, - ICANON = 51, - XCASE = 52, - ECHO = 53, - ECHOE = 54, - ECHOK = 55, - ECHONL = 56, - NOFLSH = 57, - TOSTOP = 58, - IEXTEN = 59, - ECHOCTL = 60, - ECHOKE = 61, - PENDIN = 62, - OPOST = 70, - OLCUC = 71, - ONLCR = 72, - OCRNL = 73, - ONOCR = 74, - ONLRET = 75, - - CS7 = 90, - CS8 = 91, - PARENB = 92, - PARODD = 93, - - TTY_OP_ISPEED = 128, - TTY_OP_OSPEED = 129, -} - -impl Pty { - #[doc(hidden)] - pub fn from_u8(x: u8) -> Option { - match x { - 0 => None, - 1 => Some(Pty::VINTR), - 2 => Some(Pty::VQUIT), - 3 => Some(Pty::VERASE), - 4 => Some(Pty::VKILL), - 5 => Some(Pty::VEOF), - 6 => Some(Pty::VEOL), - 7 => Some(Pty::VEOL2), - 8 => Some(Pty::VSTART), - 9 => Some(Pty::VSTOP), - 10 => Some(Pty::VSUSP), - 11 => Some(Pty::VDSUSP), - - 12 => Some(Pty::VREPRINT), - 13 => Some(Pty::VWERASE), - 14 => Some(Pty::VLNEXT), - 15 => Some(Pty::VFLUSH), - 16 => Some(Pty::VSWTCH), - 17 => Some(Pty::VSTATUS), - 18 => Some(Pty::VDISCARD), - 30 => Some(Pty::IGNPAR), - 31 => Some(Pty::PARMRK), - 32 => Some(Pty::INPCK), - 33 => Some(Pty::ISTRIP), - 34 => Some(Pty::INLCR), - 35 => Some(Pty::IGNCR), - 36 => Some(Pty::ICRNL), - 37 => Some(Pty::IUCLC), - 38 => Some(Pty::IXON), - 39 => Some(Pty::IXANY), - 40 => Some(Pty::IXOFF), - 41 => Some(Pty::IMAXBEL), - 42 => Some(Pty::IUTF8), - 50 => Some(Pty::ISIG), - 51 => Some(Pty::ICANON), - 52 => Some(Pty::XCASE), - 53 => Some(Pty::ECHO), - 54 => Some(Pty::ECHOE), - 55 => Some(Pty::ECHOK), - 56 => Some(Pty::ECHONL), - 57 => Some(Pty::NOFLSH), - 58 => Some(Pty::TOSTOP), - 59 => Some(Pty::IEXTEN), - 60 => Some(Pty::ECHOCTL), - 61 => Some(Pty::ECHOKE), - 62 => Some(Pty::PENDIN), - 70 => Some(Pty::OPOST), - 71 => Some(Pty::OLCUC), - 72 => Some(Pty::ONLCR), - 73 => Some(Pty::OCRNL), - 74 => Some(Pty::ONOCR), - 75 => Some(Pty::ONLRET), - - 90 => Some(Pty::CS7), - 91 => Some(Pty::CS8), - 92 => Some(Pty::PARENB), - 93 => Some(Pty::PARODD), - - 128 => Some(Pty::TTY_OP_ISPEED), - 129 => Some(Pty::TTY_OP_OSPEED), - _ => None, - } - } -} diff --git a/crates/bssh-russh/src/server/encrypted.rs b/crates/bssh-russh/src/server/encrypted.rs deleted file mode 100644 index 96bb2e10..00000000 --- a/crates/bssh-russh/src/server/encrypted.rs +++ /dev/null @@ -1,1526 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -use core::str; -use std::cell::RefCell; -use std::time::SystemTime; - -use auth::*; -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use cert::PublicKeyOrCertificate; -use log::{debug, error, info, trace, warn}; -use msg; -use signature::Verifier; -use ssh_encoding::{Decode, Encode, Reader}; -use ssh_key::{PublicKey, Signature}; -use tokio::time::Instant; - -use super::super::*; -use super::*; -use crate::helpers::NameList; -use crate::map_err; -use crate::msg::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED; -use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage, ensure_end}; - -impl Session { - /// Returns false iff a request was rejected. - pub(crate) async fn server_read_encrypted( - &mut self, - handler: &mut H, - pkt: &mut IncomingSshPacket, - ) -> Result<(), H::Error> { - self.process_packet(handler, &pkt.buffer).await - } - - pub(crate) async fn process_packet( - &mut self, - handler: &mut H, - buf: &[u8], - ) -> Result<(), H::Error> { - let rejection_wait_until = - tokio::time::Instant::now() + self.common.config.auth_rejection_time; - let initial_none_rejection_wait_until = if self.common.auth_attempts == 0 { - tokio::time::Instant::now() - + self - .common - .config - .auth_rejection_time_initial - .unwrap_or(self.common.config.auth_rejection_time) - } else { - rejection_wait_until - }; - - let Some(enc) = self.common.encrypted.as_mut() else { - return Err(Error::Inconsistent.into()); - }; - - // If we've successfully read a packet. - match (&mut enc.state, buf.split_first()) { - ( - EncryptedState::WaitingAuthServiceRequest { accepted, .. }, - Some((&msg::SERVICE_REQUEST, mut r)), - ) => { - let request = map_err!(String::decode(&mut r))?; - map_err!(ensure_end(&r))?; - debug!("request: {request:?}"); - if request == "ssh-userauth" { - let auth_request = server_accept_service( - handler.authentication_banner().await?, - self.common.config.as_ref().methods.clone(), - &mut enc.write, - )?; - *accepted = true; - enc.state = EncryptedState::WaitingAuthRequest(auth_request); - } - Ok(()) - } - (EncryptedState::WaitingAuthRequest(_), Some((&msg::USERAUTH_REQUEST, mut r))) => { - enc.server_read_auth_request( - rejection_wait_until, - initial_none_rejection_wait_until, - handler, - buf, - &mut r, - &mut self.common.auth_user, - ) - .await?; - self.common.auth_attempts += 1; - if let EncryptedState::InitCompression = enc.state { - if enc.client_compression.is_deferred() { - enc.client_compression.init_decompress(&mut enc.decompress); - } - handler.auth_succeeded(self).await?; - } - Ok(()) - } - ( - EncryptedState::WaitingAuthRequest(auth), - Some((&msg::USERAUTH_INFO_RESPONSE, mut r)), - ) => { - let resp = read_userauth_info_response( - rejection_wait_until, - handler, - &mut enc.write, - auth, - &self.common.auth_user, - &mut r, - ) - .await?; - if resp { - enc.state = EncryptedState::InitCompression; - if enc.client_compression.is_deferred() { - enc.client_compression.init_decompress(&mut enc.decompress); - } - handler.auth_succeeded(self).await - } else { - Ok(()) - } - } - (EncryptedState::InitCompression, Some((msg, mut r))) => { - if enc.server_compression.is_deferred() { - enc.server_compression - .init_compress(self.common.packet_writer.compress()); - } - enc.state = EncryptedState::Authenticated; - self.server_read_authenticated(handler, *msg, &mut r).await - } - (EncryptedState::Authenticated, Some((msg, mut r))) => { - self.server_read_authenticated(handler, *msg, &mut r).await - } - _ => Ok(()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::raw_no_crypto::{ - MSG_SERVICE_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_REQUEST, RawSession, - assert_rejected, capture_panics, channel_request_payload, encode_string, pty_req_payload, - raw_auth_request_signal, raw_channel_request_signal, raw_service_request_signal, - read_packet, timeout, - }; - - #[tokio::test] - async fn malformed_pty_req_truncated_modes_rejected_by_server() { - let (result, panicked) = capture_panics(async { - timeout(raw_channel_request_signal(|server_channel| { - pty_req_payload(server_channel, &[Pty::VINTR as u8, 0, 0, 0]) - })) - .await - }) - .await; - - assert!(!panicked, "truncated pty terminal modes caused a panic"); - assert_rejected(result, "truncated pty terminal modes crashed or survived"); - } - - #[tokio::test] - async fn malformed_pty_req_rejects_bytes_after_mode_end() { - let result = timeout(raw_channel_request_signal(|server_channel| { - pty_req_payload(server_channel, &[Pty::TTY_OP_END as u8, 0]) - })) - .await; - - assert_rejected( - result, - "server accepted trailing bytes inside pty terminal modes", - ); - } - - #[tokio::test] - async fn malformed_pty_req_trailing_bytes_rejected_by_server() { - let result = timeout(raw_channel_request_signal(|server_channel| { - let mut payload = pty_req_payload(server_channel, &[Pty::TTY_OP_END as u8]); - payload.push(0); - payload - })) - .await; - - assert_rejected(result, "server accepted a pty request with trailing bytes"); - } - - #[tokio::test] - async fn env_request_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_channel_request_signal(|server_channel| { - let mut payload = channel_request_payload(server_channel, b"env"); - encode_string(&mut payload, b"LANG"); - encode_string(&mut payload, b"C"); - payload.push(0); - payload - })) - .await; - - assert_rejected(result, "server accepted an env request with trailing bytes"); - } - - #[tokio::test] - async fn exec_request_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_channel_request_signal(|server_channel| { - let mut payload = channel_request_payload(server_channel, b"exec"); - encode_string(&mut payload, b"true"); - payload.push(0); - payload - })) - .await; - - assert_rejected( - result, - "server accepted an exec request with trailing bytes", - ); - } - - #[tokio::test] - async fn signal_request_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_channel_request_signal(|server_channel| { - let mut payload = channel_request_payload(server_channel, b"signal"); - encode_string(&mut payload, b"TERM"); - payload.push(0); - payload - })) - .await; - - assert_rejected( - result, - "server accepted a signal request with trailing bytes", - ); - } - - #[tokio::test] - async fn service_request_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_service_request_signal(|payload| { - payload.push(MSG_SERVICE_REQUEST); - encode_string(payload, b"ssh-userauth"); - payload.push(0); - })) - .await; - - assert_rejected( - result, - "server accepted a service request with trailing bytes", - ); - } - - #[tokio::test] - async fn auth_none_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_auth_request_signal(|payload| { - payload.push(MSG_USERAUTH_REQUEST); - encode_string(payload, b"test"); - encode_string(payload, b"ssh-connection"); - encode_string(payload, b"none"); - payload.push(0); - })) - .await; - - assert_rejected( - result, - "server accepted a none auth request with trailing bytes", - ); - } - - #[tokio::test] - async fn auth_password_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_auth_request_signal(|payload| { - payload.push(MSG_USERAUTH_REQUEST); - encode_string(payload, b"test"); - encode_string(payload, b"ssh-connection"); - encode_string(payload, b"password"); - payload.push(0); - encode_string(payload, b"secret"); - payload.push(0); - })) - .await; - - assert_rejected( - result, - "server accepted a password auth request with trailing bytes", - ); - } - - #[tokio::test] - async fn password_change_request_is_parsed_and_rejected_by_server() { - let mut stream = RawSession::connect().await; - stream.service_request().await.unwrap(); - - let mut payload = Vec::new(); - payload.push(MSG_USERAUTH_REQUEST); - encode_string(&mut payload, b"test"); - encode_string(&mut payload, b"ssh-connection"); - encode_string(&mut payload, b"password"); - payload.push(1); - encode_string(&mut payload, b"old-password"); - encode_string(&mut payload, b"new-password"); - stream.send_packet(&payload).await.unwrap(); - - let failure = read_packet(&mut stream.stream).await.unwrap(); - assert_eq!(failure.first(), Some(&MSG_USERAUTH_FAILURE)); - assert!( - !stream.events.lock().unwrap().contains(&"auth_password"), - "password-change requests should not call normal password auth" - ); - stream.server_task.abort(); - } -} - -fn server_accept_service( - banner: Option, - methods: MethodSet, - buffer: &mut Vec, -) -> Result { - push_packet!(buffer, { - buffer.push(msg::SERVICE_ACCEPT); - "ssh-userauth".encode(buffer)?; - }); - - if let Some(banner) = banner { - push_packet!(buffer, { - buffer.push(msg::USERAUTH_BANNER); - banner.encode(buffer)?; - "".encode(buffer)?; - }) - } - - Ok(AuthRequest::server(methods)) -} - -impl Encrypted { - /// Returns false iff the request was rejected. - async fn server_read_auth_request( - &mut self, - mut until: Instant, - initial_auth_until: Instant, - handler: &mut H, - original_packet: &[u8], - r: &mut &[u8], - auth_user: &mut String, - ) -> Result<(), H::Error> { - // https://tools.ietf.org/html/rfc4252#section-5 - let user = map_err!(String::decode(r))?; - let service_name = map_err!(String::decode(r))?; - let method = map_err!(String::decode(r))?; - debug!("name: {user:?} {service_name:?} {method:?}",); - - if service_name == "ssh-connection" { - { - let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state - { - a - } else { - unreachable!() - }; - if auth_request.bind_or_reset_principal(&user, &service_name) { - auth_user.clear(); - } - } - - if method == "password" { - let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state - { - a - } else { - unreachable!() - }; - auth_user.clear(); - auth_user.push_str(&user); - let change = map_err!(u8::decode(r))? != 0; - let password = map_err!(String::decode(r))?; - if change { - let _new_password = map_err!(String::decode(r))?; - } - map_err!(ensure_end(r))?; - let auth = if change { - Auth::Reject { - proceed_with_methods: None, - partial_success: false, - } - } else { - handler.auth_password(&user, &password).await? - }; - if let Auth::Accept = auth { - server_auth_request_success(&mut self.write); - self.state = EncryptedState::InitCompression; - } else { - auth_user.clear(); - if let Auth::Reject { - proceed_with_methods: Some(proceed_with_methods), - partial_success, - } = auth - { - auth_request.methods = proceed_with_methods; - auth_request.partial_success = partial_success; - } else { - auth_request.methods.remove(MethodKind::Password); - } - auth_request.partial_success = false; - reject_auth_request(until, &mut self.write, auth_request).await?; - } - Ok(()) - } else if method == "publickey" { - self.server_read_auth_request_pk( - until, - handler, - original_packet, - auth_user, - &user, - r, - ) - .await - } else if method == "none" { - let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state - { - a - } else { - unreachable!() - }; - - until = initial_auth_until; - map_err!(ensure_end(r))?; - - let auth = handler.auth_none(&user).await?; - if let Auth::Accept = auth { - server_auth_request_success(&mut self.write); - self.state = EncryptedState::InitCompression; - } else { - auth_user.clear(); - if let Auth::Reject { - proceed_with_methods: Some(proceed_with_methods), - partial_success, - } = auth - { - auth_request.methods = proceed_with_methods; - auth_request.partial_success = partial_success; - } else { - auth_request.methods.remove(MethodKind::None); - } - auth_request.partial_success = false; - reject_auth_request(until, &mut self.write, auth_request).await?; - } - Ok(()) - } else if method == "keyboard-interactive" { - let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state - { - a - } else { - unreachable!() - }; - auth_user.clear(); - auth_user.push_str(&user); - let _ = map_err!(String::decode(r))?; // language_tag, deprecated. - let submethods = map_err!(String::decode(r))?; - map_err!(ensure_end(r))?; - debug!("{submethods:?}"); - auth_request.current = Some(CurrentRequest::KeyboardInteractive { - submethods: submethods.to_string(), - }); - let auth = handler - .auth_keyboard_interactive(&user, &submethods, None) - .await?; - if reply_userauth_info_response(until, auth_request, &mut self.write, auth).await? { - self.state = EncryptedState::InitCompression - } - Ok(()) - } else { - // Other methods of the base specification are insecure or optional. - let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state - { - a - } else { - unreachable!() - }; - reject_auth_request(until, &mut self.write, auth_request).await?; - Ok(()) - } - } else { - // Unknown service - Err(Error::Inconsistent.into()) - } - } -} - -thread_local! { - static SIGNATURE_BUFFER: RefCell> = const { RefCell::new(Vec::new()) }; -} - -impl Encrypted { - async fn server_read_auth_request_pk( - &mut self, - until: Instant, - handler: &mut H, - original_packet: &[u8], - auth_user: &mut String, - user: &str, - r: &mut &[u8], - ) -> Result<(), H::Error> { - let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state { - a - } else { - unreachable!() - }; - - let is_real = map_err!(u8::decode(r))?; - - let pubkey_algo = map_err!(String::decode(r))?; - let pubkey_key = map_err!(Bytes::decode(r))?; - let key_or_cert = PublicKeyOrCertificate::decode(&pubkey_algo, &pubkey_key); - - // Parse the public key or certificate - match key_or_cert { - Ok(pk_or_cert) => { - debug!("is_real = {is_real:?}"); - - // Handle certificates specifically - let pubkey = match pk_or_cert { - PublicKeyOrCertificate::PublicKey { ref key, .. } => key.clone(), - PublicKeyOrCertificate::Certificate(ref cert) => { - // Validate certificate expiration - let now = SystemTime::now(); - if cert.valid_after_time().map(|t| now < t).unwrap_or_default() - || cert - .valid_before_time() - .map(|t| now > t) - .unwrap_or_default() - { - warn!("Certificate is expired or not yet valid"); - reject_auth_request(until, &mut self.write, auth_request).await?; - return Ok(()); - } - - // Verify the certificate’s signature - if cert.verify_signature().is_err() { - warn!("Certificate signature is invalid"); - reject_auth_request(until, &mut self.write, auth_request).await?; - return Ok(()); - } - - // Use certificate's public key for authentication - PublicKey::new(cert.public_key().clone(), "") - } - }; - - if is_real != 0 { - // SAFETY: both original_packet and pos0 are coming - // from the same allocation (pos0 is derived from - // a slice of the original_packet) - let sig_init_buffer = { - let pos0 = r.as_ptr(); - let init_len = unsafe { pos0.offset_from(original_packet.as_ptr()) }; - #[allow(clippy::indexing_slicing)] // length checked - &original_packet[0..init_len as usize] - }; - - let sent_pk_ok = if let Some(CurrentRequest::PublicKey { sent_pk_ok, .. }) = - auth_request.current - { - sent_pk_ok - } else { - false - }; - - let encoded_signature = map_err!(Vec::::decode(r))?; - - let mut signature_reader = encoded_signature.as_slice(); - let sig = map_err!(Signature::decode(&mut signature_reader))?; - map_err!(ensure_end(&signature_reader))?; - map_err!(ensure_end(r))?; - - let is_valid = if sent_pk_ok && user == auth_user { - true - } else if auth_user.is_empty() { - auth_user.clear(); - auth_user.push_str(user); - let auth = handler.auth_publickey_offered(user, &pubkey).await?; - auth == Auth::Accept - } else { - false - }; - - if is_valid { - let session_id = self.session_id.as_ref(); - #[allow(clippy::blocks_in_conditions)] - if SIGNATURE_BUFFER.with(|buf| { - let mut buf = buf.borrow_mut(); - buf.clear(); - map_err!(session_id.encode(&mut *buf))?; - buf.extend_from_slice(sig_init_buffer); - - Ok(Verifier::verify(&pubkey, &buf, &sig).is_ok()) - })? { - debug!("signature verified"); - let auth = match pk_or_cert { - PublicKeyOrCertificate::PublicKey { ref key, .. } => { - handler.auth_publickey(user, key).await? - } - PublicKeyOrCertificate::Certificate(ref cert) => { - handler.auth_openssh_certificate(user, cert).await? - } - }; - - if auth == Auth::Accept { - server_auth_request_success(&mut self.write); - self.state = EncryptedState::InitCompression; - } else { - if let Auth::Reject { - proceed_with_methods: Some(proceed_with_methods), - partial_success, - } = auth - { - auth_request.methods = proceed_with_methods; - auth_request.partial_success = partial_success; - } - auth_request.partial_success = false; - auth_user.clear(); - reject_auth_request(until, &mut self.write, auth_request).await?; - } - } else { - debug!("signature wrong"); - reject_auth_request(until, &mut self.write, auth_request).await?; - } - } else { - reject_auth_request(until, &mut self.write, auth_request).await?; - } - Ok(()) - } else { - map_err!(ensure_end(r))?; - auth_user.clear(); - auth_user.push_str(user); - let auth = handler.auth_publickey_offered(user, &pubkey).await?; - match auth { - Auth::Accept => { - let mut public_key = Vec::new(); - public_key.extend_from_slice(&pubkey_key); - - let mut algo = Vec::new(); - algo.extend_from_slice(pubkey_algo.as_bytes()); - debug!("pubkey_key: {pubkey_key:?}"); - push_packet!(self.write, { - self.write.push(msg::USERAUTH_PK_OK); - map_err!(pubkey_algo.encode(&mut self.write))?; - map_err!(pubkey_key.encode(&mut self.write))?; - }); - - auth_request.current = Some(CurrentRequest::PublicKey { - key: public_key, - algo, - sent_pk_ok: true, - }); - } - auth => { - if let Auth::Reject { - proceed_with_methods: Some(proceed_with_methods), - partial_success, - } = auth - { - auth_request.methods = proceed_with_methods; - auth_request.partial_success = partial_success; - } - auth_request.partial_success = false; - auth_user.clear(); - reject_auth_request(until, &mut self.write, auth_request).await?; - } - } - Ok(()) - } - } - Err(e) => match e { - ssh_key::Error::AlgorithmUnknown - | ssh_key::Error::AlgorithmUnsupported { .. } - | ssh_key::Error::CertificateValidation => { - debug!("public key error: {e}"); - reject_auth_request(until, &mut self.write, auth_request).await?; - Ok(()) - } - e => Err(crate::Error::from(e).into()), - }, - } - } -} - -async fn reject_auth_request( - until: Instant, - write: &mut Vec, - auth_request: &mut AuthRequest, -) -> Result<(), Error> { - debug!("rejecting {auth_request:?}"); - push_packet!(write, { - write.push(msg::USERAUTH_FAILURE); - NameList::from(&auth_request.methods).encode(write)?; - write.push(auth_request.partial_success as u8); - }); - auth_request.current = None; - auth_request.rejection_count += 1; - debug!("packet pushed"); - tokio::time::sleep_until(until).await; - Ok(()) -} - -fn server_auth_request_success(buffer: &mut Vec) { - push_packet!(buffer, { - buffer.push(msg::USERAUTH_SUCCESS); - }) -} - -async fn read_userauth_info_response( - until: Instant, - handler: &mut H, - write: &mut Vec, - auth_request: &mut AuthRequest, - user: &str, - r: &mut R, -) -> Result { - if let Some(CurrentRequest::KeyboardInteractive { ref submethods }) = auth_request.current { - let n = map_err!(u32::decode(r))?; - - // Bound both allocation and iteration by remaining packet data to - // prevent a malicious client from causing a multi-GB allocation or - // billions of loop iterations with a crafted count. - // Each response needs at least 4 bytes (length prefix). - let max_responses = r.remaining_len().saturating_add(3) / 4; - let n = (n as usize).min(max_responses); - let mut responses = Vec::with_capacity(n); - for _ in 0..n { - responses.push(Bytes::decode(r).ok()) - } - map_err!(ensure_end(r))?; - - let auth = handler - .auth_keyboard_interactive(user, submethods, Some(Response(&mut responses.into_iter()))) - .await?; - let resp = reply_userauth_info_response(until, auth_request, write, auth) - .await - .map_err(H::Error::from)?; - Ok(resp) - } else { - reject_auth_request(until, write, auth_request).await?; - Ok(false) - } -} - -async fn reply_userauth_info_response( - until: Instant, - auth_request: &mut AuthRequest, - write: &mut Vec, - auth: Auth, -) -> Result { - match auth { - Auth::Accept => { - server_auth_request_success(write); - Ok(true) - } - Auth::Reject { - proceed_with_methods, - partial_success, - } => { - if let Some(proceed_with_methods) = proceed_with_methods { - auth_request.methods = proceed_with_methods; - } - auth_request.partial_success = partial_success; - reject_auth_request(until, write, auth_request).await?; - Ok(false) - } - Auth::Partial { - name, - instructions, - prompts, - } => { - push_packet!(write, { - msg::USERAUTH_INFO_REQUEST.encode(write)?; - name.as_ref().encode(write)?; - instructions.as_ref().encode(write)?; - "".encode(write)?; // lang, should be empty - prompts.len().encode(write)?; - for &(ref a, b) in prompts.iter() { - a.as_ref().encode(write)?; - (b as u8).encode(write)?; - } - Ok::<(), crate::Error>(()) - })?; - Ok(false) - } - Auth::UnsupportedMethod => Err(Error::UnsupportedAuthMethod), - } -} - -impl Session { - async fn server_read_authenticated( - &mut self, - handler: &mut H, - msg: u8, - r: &mut R, - ) -> Result<(), H::Error> { - match msg { - msg::CHANNEL_OPEN => self - .server_handle_channel_open(handler, r) - .await - .map(|_| ()), - msg::CHANNEL_CLOSE => { - let channel_num = map_err!(ChannelId::decode(r))?; - map_err!(ensure_end(r))?; - if let Some(ref mut enc) = self.common.encrypted { - enc.channels.remove(&channel_num); - } - // Forward the close to the channel before removing it, so that - // consumers waiting on `Channel::wait()` receive an explicit - // `ChannelMsg::Close` instead of just seeing `None`. - if let Some(chan) = self.channels.get(&channel_num) { - chan.send(ChannelMsg::Close).await.unwrap_or(()) - } - self.channels.remove(&channel_num); - debug!("handler.channel_close {channel_num:?}"); - handler.channel_close(channel_num, self).await - } - msg::CHANNEL_EOF => { - let channel_num = map_err!(ChannelId::decode(r))?; - map_err!(ensure_end(r))?; - if let Some(chan) = self.channels.get(&channel_num) { - chan.send(ChannelMsg::Eof).await.unwrap_or(()) - } - debug!("handler.channel_eof {channel_num:?}"); - handler.channel_eof(channel_num, self).await - } - msg::CHANNEL_EXTENDED_DATA | msg::CHANNEL_DATA => { - let channel_num = map_err!(ChannelId::decode(r))?; - - let ext = if msg == msg::CHANNEL_DATA { - None - } else { - Some(map_err!(u32::decode(r))?) - }; - trace!("handler.data {ext:?} {channel_num:?}"); - let data = map_err!(Bytes::decode(r))?; - map_err!(ensure_end(r))?; - let target = self.target_window_size; - - if let Some(ref mut enc) = self.common.encrypted { - if enc.adjust_window_size(channel_num, &data, target)? { - let window = handler.adjust_window(channel_num, self.target_window_size); - if window > 0 { - self.target_window_size = window - } - } - } - self.flush()?; - if let Some(ext) = ext { - if let Some(chan) = self.channels.get(&channel_num) { - chan.send(ChannelMsg::ExtendedData { - ext, - data: data.clone(), - }) - .await - .unwrap_or(()) - } - handler.extended_data(channel_num, ext, &data, self).await - } else { - if let Some(chan) = self.channels.get(&channel_num) { - chan.send(ChannelMsg::Data { data: data.clone() }) - .await - .unwrap_or(()) - } - handler.data(channel_num, &data, self).await - } - } - - msg::CHANNEL_WINDOW_ADJUST => { - let channel_num = map_err!(ChannelId::decode(r))?; - let amount = map_err!(u32::decode(r))?; - map_err!(ensure_end(r))?; - let mut new_size = 0; - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get_mut(&channel_num) { - new_size = channel.recipient_window_size.saturating_add(amount); - channel.recipient_window_size = new_size; - } else { - return Ok(()); - } - } - let common = &mut self.common; - if let Some(enc) = common.encrypted.as_mut() { - new_size -= enc - .flush_pending_with_writer(&mut common.packet_writer, channel_num)? - as u32; - } - if let Some(chan) = self.channels.get(&channel_num) { - chan.window_size().update(new_size).await; - // Use try_send to avoid blocking the session loop when channel buffer is full. - // WindowAdjusted is informational - the critical side effect (updating - // WindowSizeRef and notifying ChannelTx) already happens in update(). - let _ = chan.try_send(ChannelMsg::WindowAdjusted { new_size }); - } - debug!("handler.window_adjusted {channel_num:?}"); - handler.window_adjusted(channel_num, new_size, self).await - } - - msg::CHANNEL_OPEN_CONFIRMATION => { - debug!("channel_open_confirmation"); - let msg = map_err!(ChannelOpenConfirmation::decode(r))?; - map_err!(ensure_end(r))?; - let local_id = ChannelId(msg.recipient_channel); - - if let Some(ref mut enc) = self.common.encrypted { - if let Some(parameters) = enc.channels.get_mut(&local_id) { - parameters.confirm(&msg); - } else { - // We've not requested this channel, close connection. - return Err(Error::Inconsistent.into()); - } - } else { - return Err(Error::Inconsistent.into()); - }; - - if let Some(channel) = self.channels.get(&local_id) { - channel - .send(ChannelMsg::Open { - id: local_id, - max_packet_size: msg.maximum_packet_size, - window_size: msg.initial_window_size, - }) - .await - .unwrap_or(()); - } else { - error!("no channel for id {local_id:?}"); - } - handler - .channel_open_confirmation( - local_id, - msg.maximum_packet_size, - msg.initial_window_size, - self, - ) - .await - } - - msg::CHANNEL_REQUEST => { - let channel_num = map_err!(ChannelId::decode(r))?; - let req_type = map_err!(String::decode(r))?; - let wants_reply = map_err!(u8::decode(r))?; - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get_mut(&channel_num) { - channel.wants_reply = wants_reply != 0; - } - } - match req_type.as_str() { - "pty-req" => { - let term = map_err!(String::decode(r))?; - let col_width = map_err!(u32::decode(r))?; - let row_height = map_err!(u32::decode(r))?; - let pix_width = map_err!(u32::decode(r))?; - let pix_height = map_err!(u32::decode(r))?; - let mut modes = [(Pty::TTY_OP_END, 0); 130]; - let mut i = 0; - { - let mode_string = map_err!(Bytes::decode(r))?; - let mut mode_bytes = mode_string.as_ref(); - while !mode_bytes.is_empty() { - #[allow(clippy::indexing_slicing)] // length checked - let code = mode_bytes[0]; - if code == 0 { - if mode_bytes.len() != 1 { - return Err(Error::Inconsistent.into()); - } - break; - } - if mode_bytes.len() < 5 { - return Err(Error::Inconsistent.into()); - } - #[allow(clippy::indexing_slicing)] // length checked - let num = BigEndian::read_u32(&mode_bytes[1..5]); - debug!("code = {code:?}"); - if let Some(code) = Pty::from_u8(code) { - #[allow(clippy::indexing_slicing)] // length checked - if i < 130 { - modes[i] = (code, num); - } else { - error!("pty-req: too many pty codes"); - } - } else { - info!("pty-req: unknown pty code {code:?}"); - } - i += 1; - - #[allow(clippy::indexing_slicing, reason = "length checked")] - { - mode_bytes = &mode_bytes[5..]; - } - } - } - map_err!(ensure_end(r))?; - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::RequestPty { - want_reply: true, - term: term.clone(), - col_width, - row_height, - pix_width, - pix_height, - terminal_modes: modes.into(), - }) - .await; - } - - debug!("handler.pty_request {channel_num:?}"); - #[allow(clippy::indexing_slicing)] // `modes` length checked - handler - .pty_request( - channel_num, - &term, - col_width, - row_height, - pix_width, - pix_height, - &modes[0..i], - self, - ) - .await - } - "x11-req" => { - let single_connection = map_err!(u8::decode(r))? != 0; - let x11_auth_protocol = map_err!(String::decode(r))?; - let x11_auth_cookie = map_err!(String::decode(r))?; - let x11_screen_number = map_err!(u32::decode(r))?; - map_err!(ensure_end(r))?; - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::RequestX11 { - want_reply: true, - single_connection, - x11_authentication_cookie: x11_auth_cookie.clone(), - x11_authentication_protocol: x11_auth_protocol.clone(), - x11_screen_number, - }) - .await; - } - debug!("handler.x11_request {channel_num:?}"); - handler - .x11_request( - channel_num, - single_connection, - &x11_auth_protocol, - &x11_auth_cookie, - x11_screen_number, - self, - ) - .await - } - "env" => { - let env_variable = map_err!(String::decode(r))?; - let env_value = map_err!(String::decode(r))?; - map_err!(ensure_end(r))?; - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::SetEnv { - want_reply: true, - variable_name: env_variable.clone(), - variable_value: env_value.clone(), - }) - .await; - } - - debug!("handler.env_request {channel_num:?}"); - handler - .env_request(channel_num, &env_variable, &env_value, self) - .await - } - "shell" => { - map_err!(ensure_end(r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::RequestShell { want_reply: true }) - .await; - } - debug!("handler.shell_request {channel_num:?}"); - handler.shell_request(channel_num, self).await - } - "auth-agent-req@openssh.com" => { - map_err!(ensure_end(r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::AgentForward { want_reply: true }) - .await; - } - debug!("handler.agent_request {channel_num:?}"); - - let response = handler.agent_request(channel_num, self).await?; - if response { - self.request_success() - } else { - self.request_failure() - } - Ok(()) - } - "exec" => { - let req = map_err!(Bytes::decode(r))?; - map_err!(ensure_end(r))?; - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::Exec { - want_reply: true, - command: req.to_vec(), - }) - .await; - } - debug!("handler.exec_request {channel_num:?}"); - handler.exec_request(channel_num, &req, self).await - } - "subsystem" => { - let name = map_err!(String::decode(r))?; - map_err!(ensure_end(r))?; - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::RequestSubsystem { - want_reply: true, - name: name.clone(), - }) - .await; - } - debug!("handler.subsystem_request {channel_num:?}"); - handler.subsystem_request(channel_num, &name, self).await - } - "window-change" => { - let col_width = map_err!(u32::decode(r))?; - let row_height = map_err!(u32::decode(r))?; - let pix_width = map_err!(u32::decode(r))?; - let pix_height = map_err!(u32::decode(r))?; - map_err!(ensure_end(r))?; - - if let Some(chan) = self.channels.get(&channel_num) { - let _ = chan - .send(ChannelMsg::WindowChange { - col_width, - row_height, - pix_width, - pix_height, - }) - .await; - } - - debug!("handler.window_change {channel_num:?}"); - handler - .window_change_request( - channel_num, - col_width, - row_height, - pix_width, - pix_height, - self, - ) - .await - } - "signal" => { - let signal = Sig::from_name(&map_err!(String::decode(r))?); - map_err!(ensure_end(r))?; - if let Some(chan) = self.channels.get(&channel_num) { - chan.send(ChannelMsg::Signal { - signal: signal.clone(), - }) - .await - .unwrap_or(()) - } - debug!("handler.signal {channel_num:?} {signal:?}"); - handler.signal(channel_num, signal, self).await - } - x => { - warn!("unknown channel request {x}"); - self.channel_failure(channel_num)?; - Ok(()) - } - } - } - msg::GLOBAL_REQUEST => { - let req_type = map_err!(String::decode(r))?; - self.common.wants_reply = map_err!(u8::decode(r))? != 0; - match req_type.as_str() { - "tcpip-forward" => { - let address = map_err!(String::decode(r))?; - let port = map_err!(u32::decode(r))?; - map_err!(ensure_end(r))?; - debug!("handler.tcpip_forward {address:?} {port:?}"); - let mut returned_port = port; - let result = handler - .tcpip_forward(&address, &mut returned_port, self) - .await?; - if let Some(ref mut enc) = self.common.encrypted { - if result { - push_packet!(enc.write, { - enc.write.push(msg::REQUEST_SUCCESS); - if self.common.wants_reply && port == 0 && returned_port != 0 { - map_err!(returned_port.encode(&mut enc.write))?; - } - }) - } else { - push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) - } - } - Ok(()) - } - "cancel-tcpip-forward" => { - let address = map_err!(String::decode(r))?; - let port = map_err!(u32::decode(r))?; - map_err!(ensure_end(r))?; - debug!("handler.cancel_tcpip_forward {address:?} {port:?}"); - let result = handler.cancel_tcpip_forward(&address, port, self).await?; - if let Some(ref mut enc) = self.common.encrypted { - if result { - push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) - } else { - push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) - } - } - Ok(()) - } - "streamlocal-forward@openssh.com" => { - let server_socket_path = map_err!(String::decode(r))?; - map_err!(ensure_end(r))?; - debug!("handler.streamlocal_forward {server_socket_path:?}"); - let result = handler - .streamlocal_forward(&server_socket_path, self) - .await?; - if let Some(ref mut enc) = self.common.encrypted { - if result { - push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) - } else { - push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) - } - } - Ok(()) - } - "cancel-streamlocal-forward@openssh.com" => { - let socket_path = map_err!(String::decode(r))?; - map_err!(ensure_end(r))?; - debug!("handler.cancel_streamlocal_forward {socket_path:?}"); - let result = handler - .cancel_streamlocal_forward(&socket_path, self) - .await?; - if let Some(ref mut enc) = self.common.encrypted { - if result { - push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) - } else { - push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) - } - } - Ok(()) - } - _ => { - if let Some(ref mut enc) = self.common.encrypted { - push_packet!(enc.write, { - enc.write.push(msg::REQUEST_FAILURE); - }); - } - Ok(()) - } - } - } - msg::CHANNEL_OPEN_FAILURE => { - debug!("channel_open_failure"); - let channel_num = map_err!(ChannelId::decode(r))?; - let reason = ChannelOpenFailure::from_u32(map_err!(u32::decode(r))?) - .unwrap_or(ChannelOpenFailure::Unknown); - let description = map_err!(String::decode(r))?; - let language_tag = map_err!(String::decode(r))?; - map_err!(ensure_end(r))?; - - trace!("Channel open failure description: {description}"); - trace!("Channel open failure language tag: {language_tag}"); - - if let Some(ref mut enc) = self.common.encrypted { - enc.channels.remove(&channel_num); - } - - if let Some(channel_sender) = self.channels.remove(&channel_num) { - channel_sender - .send(ChannelMsg::OpenFailure(reason)) - .await - .map_err(|_| crate::Error::SendError)?; - } - - Ok(()) - } - msg::REQUEST_SUCCESS => { - trace!("Global Request Success"); - match self.open_global_requests.pop_front() { - Some(GlobalRequestResponse::Keepalive) => { - map_err!(ensure_end(r))?; - // ignore keepalives - } - Some(GlobalRequestResponse::Ping(return_channel)) => { - map_err!(ensure_end(r))?; - let _ = return_channel.send(()); - } - Some(GlobalRequestResponse::TcpIpForward(return_channel)) => { - let result = if r.is_finished() { - // If a specific port was requested, the reply has no data - Some(0) - } else { - match u32::decode(r) { - Ok(port) => { - if let Err(e) = ensure_end(r) { - error!( - "Error parsing port for TcpIpForward request: {e:?}" - ); - None - } else { - Some(port) - } - } - Err(e) => { - error!("Error parsing port for TcpIpForward request: {e:?}"); - None - } - } - }; - let _ = return_channel.send(result); - } - Some(GlobalRequestResponse::CancelTcpIpForward(return_channel)) => { - map_err!(ensure_end(r))?; - let _ = return_channel.send(true); - } - _ => { - error!("Received global request failure for unknown request!") - } - } - Ok(()) - } - msg::REQUEST_FAILURE => { - trace!("global request failure"); - map_err!(ensure_end(r))?; - match self.open_global_requests.pop_front() { - Some(GlobalRequestResponse::Keepalive) => { - // ignore keepalives - } - Some(GlobalRequestResponse::Ping(return_channel)) => { - let _ = return_channel.send(()); - } - Some(GlobalRequestResponse::TcpIpForward(return_channel)) => { - let _ = return_channel.send(None); - } - Some(GlobalRequestResponse::CancelTcpIpForward(return_channel)) => { - let _ = return_channel.send(false); - } - _ => { - error!("Received global request failure for unknown request!") - } - } - Ok(()) - } - m => { - debug!("unknown message received: {m:?}"); - Ok(()) - } - } - } - - async fn server_handle_channel_open( - &mut self, - handler: &mut H, - r: &mut R, - ) -> Result { - let msg = OpenChannelMessage::parse(r)?; - - let sender_channel = if let Some(ref mut enc) = self.common.encrypted { - enc.new_channel_id() - } else { - unreachable!() - }; - let channel_params = ChannelParams { - recipient_channel: msg.recipient_channel, - - // "sender" is the local end, i.e. we're the sender, the remote is the recipient. - sender_channel, - - recipient_window_size: msg.recipient_window_size, - sender_window_size: self.common.config.window_size, - recipient_maximum_packet_size: msg.recipient_maximum_packet_size, - sender_maximum_packet_size: self.common.config.maximum_packet_size, - confirmed: true, - wants_reply: false, - pending_data: std::collections::VecDeque::new(), - pending_eof: false, - pending_close: false, - }; - - let (channel, reference) = Channel::new( - sender_channel, - self.sender.sender.clone(), - channel_params.recipient_maximum_packet_size, - channel_params.recipient_window_size, - self.common.config.channel_buffer_size, - ); - - match &msg.typ { - ChannelType::Session => { - let mut result = handler.channel_open_session(channel, self).await; - if let Ok(allowed) = &mut result { - self.channels.insert(sender_channel, reference); - self.finalize_channel_open(&msg, channel_params, *allowed)?; - } - result - } - ChannelType::X11 { - originator_address, - originator_port, - } => { - let mut result = handler - .channel_open_x11(channel, originator_address, *originator_port, self) - .await; - if let Ok(allowed) = &mut result { - self.channels.insert(sender_channel, reference); - self.finalize_channel_open(&msg, channel_params, *allowed)?; - } - result - } - ChannelType::DirectTcpip(d) => { - let mut result = handler - .channel_open_direct_tcpip( - channel, - &d.host_to_connect, - d.port_to_connect, - &d.originator_address, - d.originator_port, - self, - ) - .await; - if let Ok(allowed) = &mut result { - self.channels.insert(sender_channel, reference); - self.finalize_channel_open(&msg, channel_params, *allowed)?; - } - result - } - ChannelType::ForwardedTcpIp(d) => { - let mut result = handler - .channel_open_forwarded_tcpip( - channel, - &d.host_to_connect, - d.port_to_connect, - &d.originator_address, - d.originator_port, - self, - ) - .await; - if let Ok(allowed) = &mut result { - self.channels.insert(sender_channel, reference); - self.finalize_channel_open(&msg, channel_params, *allowed)?; - } - result - } - ChannelType::DirectStreamLocal(d) => { - let mut result = handler - .channel_open_direct_streamlocal(channel, &d.socket_path, self) - .await; - if let Ok(allowed) = &mut result { - self.channels.insert(sender_channel, reference); - self.finalize_channel_open(&msg, channel_params, *allowed)?; - } - result - } - ChannelType::ForwardedStreamLocal(_) => { - if let Some(ref mut enc) = self.common.encrypted { - msg.fail( - &mut enc.write, - msg::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, - b"Unsupported channel type", - )?; - } - Ok(false) - } - ChannelType::AgentForward => { - if let Some(ref mut enc) = self.common.encrypted { - msg.fail( - &mut enc.write, - msg::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, - b"Unsupported channel type", - )?; - } - Ok(false) - } - ChannelType::Unknown { typ } => { - debug!("unknown channel type: {typ}"); - if let Some(ref mut enc) = self.common.encrypted { - msg.unknown_type(&mut enc.write)?; - } - Ok(false) - } - } - } - - fn finalize_channel_open( - &mut self, - open: &OpenChannelMessage, - channel: ChannelParams, - allowed: bool, - ) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - if allowed { - open.confirm( - &mut enc.write, - channel.sender_channel.0, - channel.sender_window_size, - channel.sender_maximum_packet_size, - )?; - enc.channels.insert(channel.sender_channel, channel); - } else { - open.fail( - &mut enc.write, - SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, - b"Rejected", - )?; - } - } - Ok(()) - } -} diff --git a/crates/bssh-russh/src/server/kex.rs b/crates/bssh-russh/src/server/kex.rs deleted file mode 100644 index 6dc52e50..00000000 --- a/crates/bssh-russh/src/server/kex.rs +++ /dev/null @@ -1,399 +0,0 @@ -use core::fmt; -use std::cell::RefCell; - -use client::GexParams; -use log::debug; -use num_bigint::BigUint; -use ssh_encoding::Encode; -use ssh_key::Algorithm; - -use super::*; -use crate::helpers::sign_with_hash_alg; -use crate::kex::dh::biguint_to_mpint; -use crate::kex::{KEXES, KexAlgorithm, KexAlgorithmImplementor, KexCause}; -use crate::keys::key::PrivateKeyWithHashAlg; -use crate::negotiation::{Names, Select, is_key_compatible_with_algo}; -use crate::parsing::ensure_end; -use crate::{msg, negotiation}; - -thread_local! { - static HASH_BUF: RefCell = RefCell::new(CryptoVec::new()); -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -enum ServerKexState { - Created, - WaitingForGexRequest { - names: Names, - kex: KexAlgorithm, - }, - WaitingForDhInit { - // both KexInit and DH init sent - names: Names, - kex: KexAlgorithm, - }, - WaitingForNewKeys { - newkeys: NewKeys, - }, -} - -pub(crate) struct ServerKex { - exchange: Exchange, - cause: KexCause, - state: ServerKexState, - config: Arc, -} - -impl Debug for ServerKex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut s = f.debug_struct("ClientKex"); - s.field("cause", &self.cause); - match self.state { - ServerKexState::Created => { - s.field("state", &"created"); - } - ServerKexState::WaitingForGexRequest { .. } => { - s.field("state", &"waiting for GEX request"); - } - ServerKexState::WaitingForDhInit { .. } => { - s.field("state", &"waiting for DH reply"); - } - ServerKexState::WaitingForNewKeys { .. } => { - s.field("state", &"waiting for NEWKEYS"); - } - } - s.finish() - } -} - -impl ServerKex { - pub fn new( - config: Arc, - client_sshid: &[u8], - server_sshid: &SshId, - cause: KexCause, - ) -> Self { - let exchange = Exchange::new(client_sshid, server_sshid.as_kex_hash_bytes()); - Self { - config, - exchange, - cause, - state: ServerKexState::Created, - } - } - - pub fn kexinit(&mut self, output: &mut PacketWriter) -> Result<(), Error> { - self.exchange.server_kex_init = - negotiation::write_kex(&self.config.preferred, output, Some(self.config.as_ref()))?; - - Ok(()) - } - - pub async fn step( - mut self, - input: Option<&mut IncomingSshPacket>, - output: &mut PacketWriter, - handler: &mut H, - ) -> Result, H::Error> { - match self.state { - ServerKexState::Created => { - let Some(input) = input else { - return Err(Error::KexInit)?; - }; - if input.buffer.first() != Some(&msg::KEXINIT) { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::KexInit)?; - } - - let names = { - self.exchange.client_kex_init = input.buffer.clone().into(); - negotiation::Server::read_kex( - &input.buffer, - &self.config.preferred, - Some(&self.config.keys), - &self.cause, - )? - }; - debug!("negotiated: {names:?}"); - - // seqno has already been incremented after read() - if names.strict_kex() && !self.cause.is_rekey() && input.seqn.0 != 1 { - return Err(strict_kex_violation( - msg::KEXINIT, - input.seqn.0 as usize - 1, - ))?; - } - - let kex = KEXES.get(&names.kex).ok_or(Error::UnknownAlgo)?.make(); - - if kex.skip_exchange() { - let newkeys = compute_keys( - Vec::new(), - kex, - names.clone(), - self.exchange.clone(), - self.cause.session_id(), - )?; - - output.write_packet(|w| { - msg::NEWKEYS.encode(w)?; - Ok(()) - })?; - - return Ok(KexProgress::Done { - newkeys, - server_host_key: None, - }); - } - - if kex.is_dh_gex() { - self.state = ServerKexState::WaitingForGexRequest { names, kex }; - } else { - self.state = ServerKexState::WaitingForDhInit { names, kex }; - } - - Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn: false, - }) - } - ServerKexState::WaitingForGexRequest { names, mut kex } => { - let Some(input) = input else { - return Err(Error::KexInit)?; - }; - if input.buffer.first() != Some(&msg::KEX_DH_GEX_REQUEST) { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::KexInit)?; - } - - #[allow(clippy::indexing_slicing)] // length checked - let mut r = &input.buffer[1..]; - let gex_params = GexParams::decode(&mut r)?; - ensure_end(&r)?; - debug!("client requests a gex group: {gex_params:?}"); - - let Some(dh_group) = handler.lookup_dh_gex_group(&gex_params).await? else { - debug!( - "server::Handler impl did not find a matching DH group (is lookup_dh_gex_group implemented?)" - ); - return Err(Error::Kex)?; - }; - - let prime = biguint_to_mpint(&BigUint::from_bytes_be(&dh_group.prime)); - let generator = biguint_to_mpint(&BigUint::from_bytes_be(&dh_group.generator)); - - self.exchange.gex = Some((gex_params, dh_group.clone())); - kex.dh_gex_set_group(dh_group)?; - - output.write_packet(|w| { - msg::KEX_DH_GEX_GROUP.encode(w)?; - prime.encode(w)?; - generator.encode(w)?; - Ok(()) - })?; - - self.state = ServerKexState::WaitingForDhInit { names, kex }; - - Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn: false, - }) - } - ServerKexState::WaitingForDhInit { mut names, mut kex } => { - let Some(input) = input else { - return Err(Error::KexInit)?; - }; - - if names.ignore_guessed { - // Ignore the next packet if (1) it follows and (2) it's not the correct guess. - debug!("ignoring guessed kex"); - names.ignore_guessed = false; - self.state = ServerKexState::WaitingForDhInit { names, kex }; - return Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn: false, - }); - } - - if input.buffer.first() - != Some(match kex.is_dh_gex() { - true => &msg::KEX_DH_GEX_INIT, - false => &msg::KEX_ECDH_INIT, - }) - { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::KexInit)?; - } - - #[allow(clippy::indexing_slicing)] // length checked - let mut r = &input.buffer[1..]; - - self.exchange - .client_ephemeral - .extend_from_slice(&Bytes::decode(&mut r).map_err(Into::into)?); - ensure_end(&r)?; - - let exchange = &mut self.exchange; - kex.server_dh(exchange, &input.buffer)?; - - let Some(matching_key_index) = self - .config - .keys - .iter() - .position(|key| is_key_compatible_with_algo(key, &names.key)) - else { - debug!("we don't have a host key of type {:?}", names.key); - return Err(Error::UnknownKey.into()); - }; - - // Look up the key we'll be using to sign the exchange hash - #[allow(clippy::indexing_slicing)] // key index checked - let key = &self.config.keys[matching_key_index]; - let signature_hash_alg = match &names.key { - Algorithm::Rsa { hash } => *hash, - _ => None, - }; - - let hash = HASH_BUF.with(|buffer| { - let mut buffer = buffer.borrow_mut(); - buffer.clear(); - - let mut pubkey_vec = Vec::new(); - key.public_key().to_bytes()?.encode(&mut pubkey_vec)?; - - let hash = kex.compute_exchange_hash(&pubkey_vec, exchange, &mut buffer)?; - - Ok::<_, Error>(hash) - })?; - - // Hash signature - debug!("signing with key {key:?}"); - let signature = sign_with_hash_alg( - &PrivateKeyWithHashAlg::new(Arc::new(key.clone()), signature_hash_alg), - &hash, - ) - .map_err(Into::into)?; - - output.write_packet(|w| { - match kex.is_dh_gex() { - true => &msg::KEX_DH_GEX_REPLY, - false => &msg::KEX_ECDH_REPLY, - } - .encode(w)?; - key.public_key().to_bytes()?.encode(w)?; - exchange.server_ephemeral.encode(w)?; - signature.encode(w)?; - Ok(()) - })?; - - output.write_packet(|w| { - msg::NEWKEYS.encode(w)?; - Ok(()) - })?; - - let newkeys = compute_keys( - hash, - kex, - names.clone(), - self.exchange.clone(), - self.cause.session_id(), - )?; - - let reset_seqn = newkeys.names.strict_kex() || self.cause.is_strict_rekey(); - - self.state = ServerKexState::WaitingForNewKeys { newkeys }; - - Ok(KexProgress::NeedsReply { - kex: self, - reset_seqn, - }) - } - ServerKexState::WaitingForNewKeys { newkeys } => { - let Some(input) = input else { - return Err(Error::KexInit.into()); - }; - - if input.buffer.first() != Some(&msg::NEWKEYS) { - error!( - "Unexpected kex message at this stage: {:?}", - input.buffer.first() - ); - return Err(Error::Kex.into()); - } - #[allow(clippy::indexing_slicing, reason = "checked")] - let r = &input.buffer[1..]; - ensure_end(&r)?; - - debug!("new keys received"); - Ok(KexProgress::Done { - newkeys, - server_host_key: None, - }) - } - } - } -} - -fn compute_keys( - hash: Vec, - kex: KexAlgorithm, - names: Names, - exchange: Exchange, - session_id: Option<&CryptoVec>, -) -> Result { - let session_id_ref: &[u8] = match session_id { - Some(sid) => sid, - None => &hash, - }; - // Now computing keys. - let c = kex.compute_keys( - session_id_ref, - &hash, - names.cipher, - names.client_mac, - names.server_mac, - true, - )?; - let session_id_cv = match session_id { - Some(s) => s.clone(), - None => { - let mut cv = CryptoVec::new(); - cv.extend(&hash); - cv - } - }; - Ok(NewKeys { - exchange, - names, - kex, - key: 0, - cipher: c, - session_id: session_id_cv, - }) -} - -#[cfg(test)] -mod tests { - use crate::tests::raw_no_crypto::{assert_rejected, kexinit_payload, raw_kex_signal, timeout}; - - #[tokio::test] - async fn kexinit_with_trailing_bytes_rejected_by_server() { - let result = timeout(raw_kex_signal(|payload| { - payload.extend_from_slice(&kexinit_payload("none")); - payload.push(0); - })) - .await; - - assert_rejected(result, "server accepted a kexinit with trailing bytes"); - } -} diff --git a/crates/bssh-russh/src/server/mod.rs b/crates/bssh-russh/src/server/mod.rs deleted file mode 100644 index 7cbbfc51..00000000 --- a/crates/bssh-russh/src/server/mod.rs +++ /dev/null @@ -1,1170 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -//! # Writing servers -//! -//! There are two ways of accepting connections: -//! * implement the [Server](server::Server) trait and let [run_on_socket](server::Server::run_on_socket)/[run_on_address](server::Server::run_on_address) handle everything -//! * accept connections yourself and pass them to [run_stream](server::run_stream) -//! -//! In both cases, you'll first need to implement the [Handler](server::Handler) trait - -//! this is where you'll handle various events. -//! -//! Check out the following examples: -//! -//! * [Server that forwards your input to all connected clients](https://github.com/warp-tech/russh/blob/main/russh/examples/echoserver.rs) -//! * [Server handing channel processing off to a library (here, `russh-sftp`)](https://github.com/warp-tech/russh/blob/main/russh/examples/sftp_server.rs) -//! * Serving `ratatui` based TUI app to clients: [per-client](https://github.com/warp-tech/russh/blob/main/russh/examples/ratatui_app.rs), [shared](https://github.com/warp-tech/russh/blob/main/russh/examples/ratatui_shared_app.rs) - -use std; -use std::collections::{HashMap, VecDeque}; -use std::num::Wrapping; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; - -use bytes::Bytes; -use client::GexParams; -use futures::future::Future; -use log::{debug, error, info, warn}; -use msg::{is_kex_msg, validate_client_msg_strict_kex}; -use russh_util::runtime::JoinHandle; -use russh_util::time::Instant; -use ssh_key::{Certificate, PrivateKey}; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; -use tokio::net::{TcpListener, ToSocketAddrs}; -use tokio::pin; -use tokio::sync::{broadcast, mpsc}; - -use crate::cipher::{clear, OpeningKey}; -use crate::kex::dh::groups::{DhGroup, BUILTIN_SAFE_DH_GROUPS, DH_GROUP14}; -use crate::kex::{KexProgress, SessionKexState}; -use crate::session::*; -use crate::ssh_read::*; -use crate::sshbuffer::*; -use crate::{*}; - -mod kex; -mod session; -pub use self::session::*; -mod encrypted; - -/// Configuration of a server. -pub struct Config { - /// The server ID string sent at the beginning of the protocol. - pub server_id: SshId, - /// Authentication methods proposed to the client. - pub methods: auth::MethodSet, - /// Authentication rejections must happen in constant time for - /// security reasons. Russh does not handle this by default. - pub auth_rejection_time: std::time::Duration, - /// Authentication rejection time override for the initial "none" auth attempt. - /// OpenSSH clients will send an initial "none" auth to probe for authentication methods. - pub auth_rejection_time_initial: Option, - /// The server's keys. The first key pair in the client's preference order will be chosen. - pub keys: Vec, - /// The bytes and time limits before key re-exchange. - pub limits: Limits, - /// The initial size of a channel (used for flow control). - pub window_size: u32, - /// The maximal size of a single packet. - pub maximum_packet_size: u32, - /// Buffer size for each channel (a number of unprocessed messages to store before propagating backpressure to the TCP stream) - pub channel_buffer_size: usize, - /// Internal event buffer size - pub event_buffer_size: usize, - /// Lists of preferred algorithms. - pub preferred: Preferred, - /// Maximal number of allowed authentication attempts. - pub max_auth_attempts: usize, - /// Time after which the connection is garbage-collected. - pub inactivity_timeout: Option, - /// If nothing is received from the client for this amount of time, send a keepalive message. - pub keepalive_interval: Option, - /// If this many keepalives have been sent without reply, close the connection. - pub keepalive_max: usize, - /// If active, invoke `set_nodelay(true)` on client sockets; disabled by default (i.e. Nagle's algorithm is active). - pub nodelay: bool, -} - -impl Default for Config { - fn default() -> Config { - Config { - server_id: SshId::Standard(Cow::Borrowed(concat!( - "SSH-2.0-", - env!("CARGO_PKG_NAME"), - "_", - env!("CARGO_PKG_VERSION") - ))), - methods: auth::MethodSet::all(), - auth_rejection_time: std::time::Duration::from_secs(1), - auth_rejection_time_initial: None, - keys: Vec::new(), - window_size: 2097152, - maximum_packet_size: 32768, - channel_buffer_size: 100, - event_buffer_size: 10, - limits: Limits::default(), - preferred: Default::default(), - max_auth_attempts: 10, - inactivity_timeout: Some(std::time::Duration::from_secs(600)), - keepalive_interval: None, - keepalive_max: 3, - nodelay: false, - } - } -} - -impl Debug for Config { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // display everything except the private keys - f.debug_struct("Config") - .field("server_id", &self.server_id) - .field("methods", &self.methods) - .field("auth_rejection_time", &self.auth_rejection_time) - .field( - "auth_rejection_time_initial", - &self.auth_rejection_time_initial, - ) - .field("keys", &"***") - .field("window_size", &self.window_size) - .field("maximum_packet_size", &self.maximum_packet_size) - .field("channel_buffer_size", &self.channel_buffer_size) - .field("event_buffer_size", &self.event_buffer_size) - .field("limits", &self.limits) - .field("preferred", &self.preferred) - .field("max_auth_attempts", &self.max_auth_attempts) - .field("inactivity_timeout", &self.inactivity_timeout) - .field("keepalive_interval", &self.keepalive_interval) - .field("keepalive_max", &self.keepalive_max) - .finish() - } -} - -/// A client's response in a challenge-response authentication. -/// -/// You should iterate it to get `&[u8]` response slices. -pub struct Response<'a>(&'a mut (dyn Iterator> + Send)); - -impl Iterator for Response<'_> { - type Item = Bytes; - fn next(&mut self) -> Option { - self.0.next().flatten() - } -} - -use std::borrow::Cow; -/// An authentication result, in a challenge-response authentication. -#[derive(Debug, PartialEq, Eq)] -pub enum Auth { - /// Reject the authentication request. - Reject { - proceed_with_methods: Option, - partial_success: bool, - }, - /// Accept the authentication request. - Accept, - - /// Method was not accepted, but no other check was performed. - UnsupportedMethod, - - /// Partially accept the challenge-response authentication - /// request, providing more instructions for the client to follow. - Partial { - /// Name of this challenge. - name: Cow<'static, str>, - /// Instructions for this challenge. - instructions: Cow<'static, str>, - /// A number of prompts to the user. Each prompt has a `bool` - /// indicating whether the terminal must echo the characters - /// typed by the user. - prompts: Cow<'static, [(Cow<'static, str>, bool)]>, - }, -} - -impl Auth { - pub fn reject() -> Self { - Auth::Reject { - proceed_with_methods: None, - partial_success: false, - } - } -} - -/// Server handler. Each client will have their own handler. -/// -/// Note: this is an async trait. The trait functions return `impl Future`, -/// and you can simply define them as `async fn` instead. -#[cfg_attr(feature = "async-trait", async_trait::async_trait)] -pub trait Handler: Sized { - type Error: From + Send; - - /// Check authentication using the "none" method. Russh makes - /// sure rejection happens in time `config.auth_rejection_time`, - /// except if this method takes more than that. - #[allow(unused_variables)] - fn auth_none(&mut self, user: &str) -> impl Future> + Send { - async { Ok(Auth::reject()) } - } - - /// Check authentication using the "password" method. Russh - /// makes sure rejection happens in time - /// `config.auth_rejection_time`, except if this method takes more - /// than that. - #[allow(unused_variables)] - fn auth_password( - &mut self, - user: &str, - password: &str, - ) -> impl Future> + Send { - async { Ok(Auth::reject()) } - } - - /// Check authentication using the "publickey" method. This method - /// should just check whether the public key matches the - /// authorized ones. Russh then checks the signature. If the key - /// is unknown, or the signature is invalid, Russh guarantees - /// that rejection happens in constant time - /// `config.auth_rejection_time`, except if this method takes more - /// time than that. - #[allow(unused_variables)] - fn auth_publickey_offered( - &mut self, - user: &str, - public_key: &ssh_key::PublicKey, - ) -> impl Future> + Send { - async { Ok(Auth::Accept) } - } - - /// Check authentication using the "publickey" method. This method - /// is called after the signature has been verified and key - /// ownership has been confirmed. - /// Russh guarantees that rejection happens in constant time - /// `config.auth_rejection_time`, except if this method takes more - /// time than that. - #[allow(unused_variables)] - fn auth_publickey( - &mut self, - user: &str, - public_key: &ssh_key::PublicKey, - ) -> impl Future> + Send { - async { Ok(Auth::reject()) } - } - - /// Check authentication using an OpenSSH certificate. This method - /// is called after the signature has been verified and key - /// ownership has been confirmed. - /// Russh guarantees that rejection happens in constant time - /// `config.auth_rejection_time`, except if this method takes more - /// time than that. - #[allow(unused_variables)] - fn auth_openssh_certificate( - &mut self, - user: &str, - certificate: &Certificate, - ) -> impl Future> + Send { - async { Ok(Auth::reject()) } - } - - /// Check authentication using the "keyboard-interactive" - /// method. Russh makes sure rejection happens in time - /// `config.auth_rejection_time`, except if this method takes more - /// than that. - #[allow(unused_variables)] - fn auth_keyboard_interactive<'a>( - &'a mut self, - user: &str, - submethods: &str, - response: Option>, - ) -> impl Future> + Send { - async { Ok(Auth::reject()) } - } - - /// Called when authentication succeeds for a session. - #[allow(unused_variables)] - fn auth_succeeded( - &mut self, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when authentication starts but before it is successful. - /// Return value is an authentication banner, usually a warning message shown to the client. - #[allow(unused_variables)] - fn authentication_banner( - &mut self, - ) -> impl Future, Self::Error>> + Send { - async { Ok(None) } - } - - /// Called when the client closes a channel. - #[allow(unused_variables)] - fn channel_close( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the client sends EOF to a channel. - #[allow(unused_variables)] - fn channel_eof( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when a new session channel is created. - /// Return value indicates whether the channel request should be granted. - #[allow(unused_variables)] - fn channel_open_session( - &mut self, - channel: Channel, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Called when a new X11 channel is created. - /// Return value indicates whether the channel request should be granted. - #[allow(unused_variables)] - fn channel_open_x11( - &mut self, - channel: Channel, - originator_address: &str, - originator_port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Called when a new direct TCP/IP ("local TCP forwarding") channel is opened. - /// Return value indicates whether the channel request should be granted. - #[allow(unused_variables)] - fn channel_open_direct_tcpip( - &mut self, - channel: Channel, - host_to_connect: &str, - port_to_connect: u32, - originator_address: &str, - originator_port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Called when a new remote forwarded TCP connection comes in. - /// - #[allow(unused_variables)] - fn channel_open_forwarded_tcpip( - &mut self, - channel: Channel, - host_to_connect: &str, - port_to_connect: u32, - originator_address: &str, - originator_port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Called when a new direct-streamlocal ("local UNIX socket forwarding") channel is created. - /// Return value indicates whether the channel request should be granted. - #[allow(unused_variables)] - fn channel_open_direct_streamlocal( - &mut self, - channel: Channel, - socket_path: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Called when the client confirmed our request to open a - /// channel. A channel can only be written to after receiving this - /// message (this library panics otherwise). - #[allow(unused_variables)] - fn channel_open_confirmation( - &mut self, - id: ChannelId, - max_packet_size: u32, - window_size: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when a data packet is received. A response can be - /// written to the `response` argument. - #[allow(unused_variables)] - fn data( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when an extended data packet is received. Code 1 means - /// that this packet comes from stderr, other codes are not - /// defined (see - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-5.2)). - #[allow(unused_variables)] - fn extended_data( - &mut self, - channel: ChannelId, - code: u32, - data: &[u8], - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when the network window is adjusted, meaning that we - /// can send more bytes. - #[allow(unused_variables)] - fn window_adjusted( - &mut self, - channel: ChannelId, - new_size: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Called when this server adjusts the network window. Return the - /// next target window. - #[allow(unused_variables)] - fn adjust_window(&mut self, channel: ChannelId, current: u32) -> u32 { - current - } - - /// The client requests a pseudo-terminal with the given - /// specifications. - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn pty_request( - /// &mut self, - /// channel: ChannelId, - /// term: &str, - /// col_width: u32, - /// row_height: u32, - /// pix_width: u32, - /// pix_height: u32, - /// modes: &[(Pty, u32)], - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables, clippy::too_many_arguments)] - fn pty_request( - &mut self, - channel: ChannelId, - term: &str, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - modes: &[(Pty, u32)], - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client requests an X11 connection. - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn x11_request( - /// &mut self, - /// channel: ChannelId, - /// single_connection: bool, - /// x11_auth_protocol: &str, - /// x11_auth_cookie: &str, - /// x11_screen_number: u32, - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn x11_request( - &mut self, - channel: ChannelId, - single_connection: bool, - x11_auth_protocol: &str, - x11_auth_cookie: &str, - x11_screen_number: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client wants to set the given environment variable. Check - /// these carefully, as it is dangerous to allow any variable - /// environment to be set. - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn env_request( - /// &mut self, - /// channel: ChannelId, - /// variable_name: &str, - /// variable_value: &str, - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn env_request( - &mut self, - channel: ChannelId, - variable_name: &str, - variable_value: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client requests a shell. - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn shell_request( - /// &mut self, - /// channel: ChannelId, - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn shell_request( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client sends a command to execute, to be passed to a - /// shell. Make sure to check the command before doing so. - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn exec_request( - /// &mut self, - /// channel: ChannelId, - /// data: &[u8], - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn exec_request( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client asks to start the subsystem with the given name - /// (such as sftp). - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn subsystem_request( - /// &mut self, - /// channel: ChannelId, - /// name: &str, - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn subsystem_request( - &mut self, - channel: ChannelId, - name: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client's pseudo-terminal window size has changed. - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn window_change_request( - /// &mut self, - /// channel: ChannelId, - /// col_width: u32, - /// row_height: u32, - /// pix_width: u32, - /// pix_height: u32, - /// session: &mut Session, - /// ) -> Result<(), Self::Error> { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn window_change_request( - &mut self, - channel: ChannelId, - col_width: u32, - row_height: u32, - pix_width: u32, - pix_height: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// The client requests OpenSSH agent forwarding - /// - /// **Note:** Success or failure should be communicated to the client by calling - /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. For - /// instance: - /// - /// ```ignore - /// async fn agent_request( - /// &mut self, - /// channel: ChannelId, - /// session: &mut Session, - /// ) -> Result { - /// session.channel_success(channel); - /// Ok(()) - /// } - /// ``` - #[allow(unused_variables)] - fn agent_request( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// The client is sending a signal (usually to pass to the - /// currently running process). - #[allow(unused_variables)] - fn signal( - &mut self, - channel: ChannelId, - signal: Sig, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(()) } - } - - /// Used for reverse-forwarding ports, see - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). - /// If `port` is 0, you should set it to the allocated port number. - #[allow(unused_variables)] - fn tcpip_forward( - &mut self, - address: &str, - port: &mut u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Used to stop the reverse-forwarding of a port, see - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). - #[allow(unused_variables)] - fn cancel_tcpip_forward( - &mut self, - address: &str, - port: u32, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - #[allow(unused_variables)] - fn streamlocal_forward( - &mut self, - socket_path: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - #[allow(unused_variables)] - fn cancel_streamlocal_forward( - &mut self, - socket_path: &str, - session: &mut Session, - ) -> impl Future> + Send { - async { Ok(false) } - } - - /// Override when enabling the `diffie-hellman-group-exchange-*` key exchange methods. - /// Should return a Diffie-Hellman group with a safe prime whose length is - /// between `gex_params.min_group_size` and `gex_params.max_group_size` and - /// (if possible) over and as close as possible to `gex_params.preferred_group_size`. - /// - /// OpenSSH uses a pre-generated database of safe primes stored in `/etc/ssh/moduli` - /// - /// The default implementation picks a group from a very short static list - /// of built-in standard groups and is not really taking advantage of the security - /// offered by these kex methods. - /// - /// See https://datatracker.ietf.org/doc/html/rfc4419#section-3 - #[allow(unused_variables)] - fn lookup_dh_gex_group( - &mut self, - gex_params: &GexParams, - ) -> impl Future, Self::Error>> + Send { - async { - let mut best_group = &DH_GROUP14; - - // Find _some_ matching group - for group in BUILTIN_SAFE_DH_GROUPS.iter() { - if group.bit_size() >= gex_params.min_group_size() - && group.bit_size() <= gex_params.max_group_size() - { - best_group = *group; - break; - } - } - - // Find _closest_ matching group - for group in BUILTIN_SAFE_DH_GROUPS.iter() { - if group.bit_size() > gex_params.preferred_group_size() { - best_group = *group; - break; - } - } - - Ok(Some(best_group.clone())) - } - } -} - -pub struct RunningServerHandle { - shutdown_tx: broadcast::Sender, -} - -impl RunningServerHandle { - /// Request graceful server shutdown. - /// Starts the shutdown and immediately returns. - /// To wait for all the clients to disconnect, await `RunningServer` . - pub fn shutdown(&self, reason: String) { - let _ = self.shutdown_tx.send(reason); - } -} - -pub struct RunningServer> + Unpin + Send> { - inner: F, - shutdown_tx: broadcast::Sender, -} - -impl> + Unpin + Send> RunningServer { - pub fn handle(&self) -> RunningServerHandle { - RunningServerHandle { - shutdown_tx: self.shutdown_tx.clone(), - } - } -} - -impl> + Unpin + Send> Future for RunningServer { - type Output = std::io::Result<()>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - Future::poll(Pin::new(&mut self.inner), cx) - } -} - -#[cfg_attr(feature = "async-trait", async_trait::async_trait)] -/// Trait used to create new handlers when clients connect. -pub trait Server { - /// The type of handlers. - type Handler: Handler + Send + 'static; - /// Called when a new client connects. - fn new_client(&mut self, peer_addr: Option) -> Self::Handler; - /// Called when an active connection fails. - fn handle_session_error(&mut self, _error: ::Error) {} - - /// Run a server on a specified `tokio::net::TcpListener`. Useful when dropping - /// privileges immediately after socket binding, for example. - fn run_on_socket( - &mut self, - config: Arc, - socket: &TcpListener, - ) -> RunningServer> + Unpin + Send> - where - Self: Send, - { - let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1); - let shutdown_tx2 = shutdown_tx.clone(); - - let fut = async move { - if config.maximum_packet_size > 65535 { - error!( - "Maximum packet size ({:?}) should not larger than a TCP packet (65535)", - config.maximum_packet_size - ); - } - - let (error_tx, mut error_rx) = mpsc::unbounded_channel(); - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - debug!("Server shutdown requested"); - return Ok(()); - }, - accept_result = socket.accept() => { - match accept_result { - Ok((socket, peer_addr)) => { - let mut shutdown_rx = shutdown_tx2.subscribe(); - - let config = config.clone(); - // NOTE: For backwards compatibility, we keep the Option signature as changing it would be a breaking change. - let handler = self.new_client(Some(peer_addr)); - let error_tx = error_tx.clone(); - - russh_util::runtime::spawn(async move { - if config.nodelay { - if let Err(e) = socket.set_nodelay(true) { - warn!("set_nodelay() failed: {e:?}"); - } - } - - let session = match run_stream(config, socket, handler).await { - Ok(s) => s, - Err(e) => { - debug!("Connection setup failed"); - let _ = error_tx.send(e); - return - } - }; - - let handle = session.handle(); - - tokio::select! { - reason = shutdown_rx.recv() => { - if handle.disconnect( - Disconnect::ByApplication, - reason.unwrap_or_else(|_| "".into()), - "".into() - ).await.is_err() { - debug!("Failed to send disconnect message"); - } - }, - result = session => { - if let Err(e) = result { - debug!("Connection closed with error"); - let _ = error_tx.send(e); - } else { - debug!("Connection closed"); - } - } - } - }); - } - Err(e) => { - return Err(e); - } - } - }, - - Some(error) = error_rx.recv() => { - self.handle_session_error(error); - } - } - } - }; - - RunningServer { - inner: Box::pin(fut), - shutdown_tx, - } - } - - /// Run a server. - /// This is a convenience function; consider using `run_on_socket` for more control. - fn run_on_address( - &mut self, - config: Arc, - addrs: A, - ) -> impl Future> + Send - where - Self: Send, - { - async { - let socket = TcpListener::bind(addrs).await?; - self.run_on_socket(config, &socket).await?; - Ok(()) - } - } -} - -async fn start_reading( - mut stream_read: R, - mut buffer: SSHBuffer, - mut cipher: Box, -) -> Result<(usize, R, SSHBuffer, Box), Error> { - buffer.buffer.clear(); - let n = cipher::read(&mut stream_read, &mut buffer, &mut *cipher).await?; - Ok((n, stream_read, buffer, cipher)) -} - -/// An active server session returned by [run_stream]. -/// -/// Implements [Future] and can be awaited to wait for the session to finish. -pub struct RunningSession { - handle: Handle, - join: JoinHandle>, -} - -impl RunningSession { - /// Returns a new handle for the session. - pub fn handle(&self) -> Handle { - self.handle.clone() - } -} - -impl Future for RunningSession { - type Output = Result<(), H::Error>; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - match Future::poll(Pin::new(&mut self.join), cx) { - Poll::Ready(r) => Poll::Ready(match r { - Ok(Ok(x)) => Ok(x), - Err(e) => Err(crate::Error::from(e).into()), - Ok(Err(e)) => Err(e), - }), - Poll::Pending => Poll::Pending, - } - } -} - -/// Start a single connection in the background. -pub async fn run_stream( - config: Arc, - mut stream: R, - handler: H, -) -> Result, H::Error> -where - H: Handler + Send + 'static, - R: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - // Writing SSH id. - let mut write_buffer = SSHBuffer::new(); - write_buffer.send_ssh_id(&config.as_ref().server_id); - map_err!(stream.write_all(&write_buffer.buffer[..]).await)?; - - // Reading SSH id and allocating a session. - let mut stream = SshRead::new(stream); - let (sender, receiver) = tokio::sync::mpsc::channel(config.event_buffer_size); - let handle = server::session::Handle { - sender, - channel_buffer_size: config.channel_buffer_size, - }; - - let common = read_ssh_id(config, &mut stream).await?; - let mut session = Session { - target_window_size: common.config.window_size, - common, - receiver, - sender: handle.clone(), - pending_reads: Vec::new(), - pending_len: 0, - channels: HashMap::new(), - open_global_requests: VecDeque::new(), - kex: SessionKexState::Idle, - }; - - session.begin_rekey()?; - - let join = russh_util::runtime::spawn(session.run(stream, handler)); - - Ok(RunningSession { handle, join }) -} - -async fn read_ssh_id( - config: Arc, - read: &mut SshRead, -) -> Result>, Error> { - let sshid = if let Some(t) = config.inactivity_timeout { - tokio::time::timeout(t, read.read_client_ssh_id()).await?? - } else { - read.read_client_ssh_id().await? - }; - - let session = CommonSession { - packet_writer: PacketWriter::clear(), - // kex: Some(Kex::Init(kexinit)), - auth_user: String::new(), - auth_method: None, // Client only. - auth_attempts: 0, - remote_to_local: Box::new(clear::Key), - encrypted: None, - config, - wants_reply: false, - disconnected: false, - buffer: Vec::new(), - strict_kex: false, - alive_timeouts: 0, - received_data: false, - remote_sshid: sshid.into(), - }; - Ok(session) -} - -async fn reply( - session: &mut Session, - handler: &mut H, - pkt: &mut IncomingSshPacket, -) -> Result<(), H::Error> { - if let Some(message_type) = pkt.buffer.first() { - debug!( - "< msg type {message_type:?}, seqn {:?}, len {}", - pkt.seqn.0, - pkt.buffer.len() - ); - if session.common.strict_kex && session.common.encrypted.is_none() { - let seqno = pkt.seqn.0 - 1; // was incremented after read() - validate_client_msg_strict_kex(*message_type, seqno as usize)?; - } - - if [msg::IGNORE, msg::UNIMPLEMENTED, msg::DEBUG].contains(message_type) { - return Ok(()); - } - } - - if pkt.buffer.first() == Some(&msg::KEXINIT) && session.kex == SessionKexState::Idle { - // Not currently in a rekey but received KEXINIT - info!("Client has initiated re-key"); - session.begin_rekey()?; - // Kex will consume the packet right away - } - - let is_kex_msg = pkt.buffer.first().cloned().map(is_kex_msg).unwrap_or(false); - - if is_kex_msg { - if let SessionKexState::InProgress(kex) = session.kex.take() { - let progress = kex - .step(Some(pkt), &mut session.common.packet_writer, handler) - .await?; - - match progress { - KexProgress::NeedsReply { kex, reset_seqn } => { - debug!("kex impl continues: {kex:?}"); - session.kex = SessionKexState::InProgress(kex); - if reset_seqn { - debug!("kex impl requests seqno reset"); - session.common.reset_seqn(); - } - } - KexProgress::Done { newkeys, .. } => { - debug!("kex impl has completed"); - session.common.strict_kex = - session.common.strict_kex || newkeys.names.strict_kex(); - - if session.common.encrypted.is_some() { - // This is a rekey - { - let common = &mut session.common; - common.newkeys(newkeys); - common.packet_writer.buffer().bytes = 0; - if let Some(enc) = common.encrypted.as_mut() { - enc.last_rekey = Instant::now(); - enc.flush_all_pending_with_writer(&mut common.packet_writer)?; - } - } - - let mut pending = std::mem::take(&mut session.pending_reads); - for p in pending.drain(..) { - session.process_packet(handler, &p).await?; - } - session.pending_reads = pending; - session.pending_len = 0; - session.flush()?; - } else { - // This is the initial kex - - session.common.encrypted( - EncryptedState::WaitingAuthServiceRequest { - sent: false, - accepted: false, - }, - newkeys, - ); - - session.maybe_send_ext_info()?; - } - - session.kex = SessionKexState::Idle; - - if session.common.strict_kex { - pkt.seqn = Wrapping(0); - } - - debug!("kex done"); - } - } - - session.flush()?; - - return Ok(()); - } - } - - // Handle key exchange/re-exchange. - session.server_read_encrypted(handler, pkt).await -} diff --git a/crates/bssh-russh/src/server/session.rs b/crates/bssh-russh/src/server/session.rs deleted file mode 100644 index 6434ead1..00000000 --- a/crates/bssh-russh/src/server/session.rs +++ /dev/null @@ -1,1570 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::io::ErrorKind; -use std::sync::Arc; - -use channels::WindowSizeRef; -use kex::ServerKex; -use log::debug; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; -use tokio::sync::mpsc::{Receiver, Sender, channel, error::TryRecvError}; -use tokio::sync::oneshot; - -use super::*; -use crate::channels::{Channel, ChannelMsg, ChannelReadHalf, ChannelRef, ChannelWriteHalf}; -use crate::helpers::NameList; -use crate::kex::{EXTENSION_SUPPORT_AS_CLIENT, KexCause, SessionKexState}; -use crate::{map_err, msg}; - -/// A connected server session. This type is unique to a client. -#[derive(Debug)] -pub struct Session { - pub(crate) common: CommonSession>, - pub(crate) sender: Handle, - pub(crate) receiver: Receiver, - pub(crate) target_window_size: u32, - pub(crate) pending_reads: Vec>, - pub(crate) pending_len: u32, - pub(crate) channels: HashMap, - pub(crate) open_global_requests: VecDeque, - pub(crate) kex: SessionKexState, -} - -#[derive(Debug)] -pub enum Msg { - ChannelOpenAgent { - channel_ref: ChannelRef, - }, - ChannelOpenSession { - channel_ref: ChannelRef, - }, - ChannelOpenDirectTcpIp { - host_to_connect: String, - port_to_connect: u32, - originator_address: String, - originator_port: u32, - channel_ref: ChannelRef, - }, - ChannelOpenDirectStreamLocal { - socket_path: String, - channel_ref: ChannelRef, - }, - ChannelOpenForwardedTcpIp { - connected_address: String, - connected_port: u32, - originator_address: String, - originator_port: u32, - channel_ref: ChannelRef, - }, - ChannelOpenForwardedStreamLocal { - server_socket_path: String, - channel_ref: ChannelRef, - }, - ChannelOpenX11 { - originator_address: String, - originator_port: u32, - channel_ref: ChannelRef, - }, - TcpIpForward { - /// Provide a channel for the reply result to request a reply from the server - reply_channel: Option>>, - address: String, - port: u32, - }, - CancelTcpIpForward { - /// Provide a channel for the reply result to request a reply from the server - reply_channel: Option>, - address: String, - port: u32, - }, - Disconnect { - reason: crate::Disconnect, - description: String, - language_tag: String, - }, - Channel(ChannelId, ChannelMsg), -} - -impl From<(ChannelId, ChannelMsg)> for Msg { - fn from((id, msg): (ChannelId, ChannelMsg)) -> Self { - Msg::Channel(id, msg) - } -} - -#[derive(Clone, Debug)] -/// Handle to a session, used to send messages to a client outside of -/// the request/response cycle. -pub struct Handle { - pub(crate) sender: Sender, - pub(crate) channel_buffer_size: usize, -} - -impl Handle { - /// Send data to the session referenced by this handler. - pub async fn data( - &self, - id: ChannelId, - data: impl Into, - ) -> Result<(), bytes::Bytes> { - let data = data.into(); - self.sender - .send(Msg::Channel(id, ChannelMsg::Data { data })) - .await - .map_err(|e| match e.0 { - Msg::Channel(_, ChannelMsg::Data { data }) => data, - _ => unreachable!(), - }) - } - - /// Send data to the session referenced by this handler. - pub async fn extended_data( - &self, - id: ChannelId, - ext: u32, - data: impl Into, - ) -> Result<(), bytes::Bytes> { - let data = data.into(); - self.sender - .send(Msg::Channel(id, ChannelMsg::ExtendedData { ext, data })) - .await - .map_err(|e| match e.0 { - Msg::Channel(_, ChannelMsg::ExtendedData { data, .. }) => data, - _ => unreachable!(), - }) - } - - /// Send EOF to the session referenced by this handler. - pub async fn eof(&self, id: ChannelId) -> Result<(), ()> { - self.sender - .send(Msg::Channel(id, ChannelMsg::Eof)) - .await - .map_err(|_| ()) - } - - /// Send success to the session referenced by this handler. - pub async fn channel_success(&self, id: ChannelId) -> Result<(), ()> { - self.sender - .send(Msg::Channel(id, ChannelMsg::Success)) - .await - .map_err(|_| ()) - } - - /// Send failure to the session referenced by this handler. - pub async fn channel_failure(&self, id: ChannelId) -> Result<(), ()> { - self.sender - .send(Msg::Channel(id, ChannelMsg::Failure)) - .await - .map_err(|_| ()) - } - - /// Close a channel. - pub async fn close(&self, id: ChannelId) -> Result<(), ()> { - self.sender - .send(Msg::Channel(id, ChannelMsg::Close)) - .await - .map_err(|_| ()) - } - - /// Inform the client of whether they may perform - /// control-S/control-Q flow control. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.8). - pub async fn xon_xoff_request(&self, id: ChannelId, client_can_do: bool) -> Result<(), ()> { - self.sender - .send(Msg::Channel(id, ChannelMsg::XonXoff { client_can_do })) - .await - .map_err(|_| ()) - } - - /// Send the exit status of a program. - pub async fn exit_status_request(&self, id: ChannelId, exit_status: u32) -> Result<(), ()> { - self.sender - .send(Msg::Channel(id, ChannelMsg::ExitStatus { exit_status })) - .await - .map_err(|_| ()) - } - - /// Notifies the client that it can open TCP/IP forwarding channels for a port. - pub async fn forward_tcpip(&self, address: String, port: u32) -> Result { - let (reply_send, reply_recv) = oneshot::channel(); - self.sender - .send(Msg::TcpIpForward { - reply_channel: Some(reply_send), - address, - port, - }) - .await - .map_err(|_| ())?; - - match reply_recv.await { - Ok(Some(port)) => Ok(port), - Ok(None) => Err(()), // crate::Error::RequestDenied - Err(e) => { - error!("Unable to receive TcpIpForward result: {e:?}"); - Err(()) // crate::Error::Disconnect - } - } - } - - /// Notifies the client that it can no longer open TCP/IP forwarding channel for a port. - pub async fn cancel_forward_tcpip(&self, address: String, port: u32) -> Result<(), ()> { - let (reply_send, reply_recv) = oneshot::channel(); - self.sender - .send(Msg::CancelTcpIpForward { - reply_channel: Some(reply_send), - address, - port, - }) - .await - .map_err(|_| ())?; - match reply_recv.await { - Ok(true) => Ok(()), - Ok(false) => Err(()), // crate::Error::RequestDenied - Err(e) => { - error!("Unable to receive CancelTcpIpForward result: {e:?}"); - Err(()) // crate::Error::Disconnect - } - } - } - - /// Open an agent forwarding channel. This can be used once the client has - /// confirmed that it allows agent forwarding. See - /// [PROTOCOL.agent](https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent). - pub async fn channel_open_agent(&self) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenAgent { channel_ref }) - .await - .map_err(|_| Error::SendError)?; - - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - /// Request a session channel (the most basic type of - /// channel). This function returns `Ok(..)` immediately if the - /// connection is authenticated, but the channel only becomes - /// usable when it's confirmed by the server, as indicated by the - /// `confirmed` field of the corresponding `Channel`. - pub async fn channel_open_session(&self) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenSession { channel_ref }) - .await - .map_err(|_| Error::SendError)?; - - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - /// Open a TCP/IP forwarding channel. This is usually done when a - /// connection comes to a locally forwarded TCP/IP port. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). The - /// TCP/IP packets can then be tunneled through the channel using - /// `.data()`. - pub async fn channel_open_direct_tcpip, B: Into>( - &self, - host_to_connect: A, - port_to_connect: u32, - originator_address: B, - originator_port: u32, - ) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenDirectTcpIp { - host_to_connect: host_to_connect.into(), - port_to_connect, - originator_address: originator_address.into(), - originator_port, - channel_ref, - }) - .await - .map_err(|_| Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - /// Open a direct streamlocal (Unix domain socket) channel on the client. - pub async fn channel_open_direct_streamlocal>( - &self, - socket_path: A, - ) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenDirectStreamLocal { - socket_path: socket_path.into(), - channel_ref, - }) - .await - .map_err(|_| Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - pub async fn channel_open_forwarded_tcpip, B: Into>( - &self, - connected_address: A, - connected_port: u32, - originator_address: B, - originator_port: u32, - ) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenForwardedTcpIp { - connected_address: connected_address.into(), - connected_port, - originator_address: originator_address.into(), - originator_port, - channel_ref, - }) - .await - .map_err(|_| Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - pub async fn channel_open_forwarded_streamlocal>( - &self, - server_socket_path: A, - ) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenForwardedStreamLocal { - server_socket_path: server_socket_path.into(), - channel_ref, - }) - .await - .map_err(|_| Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - pub async fn channel_open_x11>( - &self, - originator_address: A, - originator_port: u32, - ) -> Result, Error> { - let (sender, receiver) = channel(self.channel_buffer_size); - let channel_ref = ChannelRef::new(sender); - let window_size_ref = channel_ref.window_size().clone(); - - self.sender - .send(Msg::ChannelOpenX11 { - originator_address: originator_address.into(), - originator_port, - channel_ref, - }) - .await - .map_err(|_| Error::SendError)?; - self.wait_channel_confirmation(receiver, window_size_ref) - .await - } - - async fn wait_channel_confirmation( - &self, - mut receiver: Receiver, - window_size_ref: WindowSizeRef, - ) -> Result, Error> { - loop { - match receiver.recv().await { - Some(ChannelMsg::Open { - id, - max_packet_size, - window_size, - }) => { - window_size_ref.update(window_size).await; - - return Ok(Channel { - write_half: ChannelWriteHalf { - id, - sender: self.sender.clone(), - max_packet_size, - window_size: window_size_ref, - }, - read_half: ChannelReadHalf { receiver }, - }); - } - Some(ChannelMsg::OpenFailure(reason)) => { - return Err(Error::ChannelOpenFailure(reason)); - } - None => { - return Err(Error::Disconnect); - } - msg => { - debug!("msg = {msg:?}"); - } - } - } - } - - /// If the program was killed by a signal, send the details about the signal to the client. - pub async fn exit_signal_request( - &self, - id: ChannelId, - signal_name: Sig, - core_dumped: bool, - error_message: String, - lang_tag: String, - ) -> Result<(), ()> { - self.sender - .send(Msg::Channel( - id, - ChannelMsg::ExitSignal { - signal_name, - core_dumped, - error_message, - lang_tag, - }, - )) - .await - .map_err(|_| ()) - } - - /// Allows a server to disconnect a client session - pub async fn disconnect( - &self, - reason: Disconnect, - description: String, - language_tag: String, - ) -> Result<(), Error> { - self.sender - .send(Msg::Disconnect { - reason, - description, - language_tag, - }) - .await - .map_err(|_| Error::SendError) - } -} - -impl Session { - fn maybe_decompress(&mut self, buffer: &SSHBuffer) -> Result { - if let Some(ref mut enc) = self.common.encrypted { - let mut decomp = Vec::new(); - Ok(IncomingSshPacket { - #[allow(clippy::indexing_slicing)] // length checked - buffer: enc.decompress.decompress( - &buffer.buffer[5..], - &mut decomp, - )?.into(), - seqn: buffer.seqn, - }) - } else { - Ok(IncomingSshPacket { - #[allow(clippy::indexing_slicing)] // length checked - buffer: buffer.buffer[5..].into(), - seqn: buffer.seqn, - }) - } - } - - pub(crate) async fn run( - mut self, - mut stream: SshRead, - mut handler: H, - ) -> Result<(), H::Error> - where - H: Handler + Send + 'static, - R: AsyncRead + AsyncWrite + Unpin + Send + 'static, - { - self.flush()?; - - map_err!(self.common.packet_writer.flush_into(&mut stream).await)?; - - let (stream_read, mut stream_write) = stream.split(); - let buffer = SSHBuffer::new(); - - // Allow handing out references to the cipher - let mut opening_cipher = Box::new(clear::Key) as Box; - std::mem::swap(&mut opening_cipher, &mut self.common.remote_to_local); - - let keepalive_timer = - future_or_pending(self.common.config.keepalive_interval, tokio::time::sleep); - pin!(keepalive_timer); - - let inactivity_timer = - future_or_pending(self.common.config.inactivity_timeout, tokio::time::sleep); - pin!(inactivity_timer); - - let reading = start_reading(stream_read, buffer, opening_cipher); - pin!(reading); - let mut is_reading = None; - - #[allow(clippy::panic)] // false positive in macro - while !self.common.disconnected { - self.common.received_data = false; - let mut sent_keepalive = false; - - // BSSH FIX: Process pending messages before entering select! - // This ensures messages sent via Handle::data() from spawned tasks - // are processed even when select! doesn't wake up for them. - // Critical for interactive PTY sessions where shell I/O runs in a separate task. - // - // We limit the number of messages processed per batch to ensure client input - // (e.g., Ctrl+C) is handled promptly even during high-throughput output. - const MAX_MESSAGES_PER_BATCH: usize = 64; - let mut processed_count = 0usize; - if !self.kex.active() { - loop { - if processed_count >= MAX_MESSAGES_PER_BATCH { - // Yield to select! to check for client input - break; - } - match self.receiver.try_recv() { - Ok(Msg::Channel(id, ChannelMsg::Data { data })) => { - self.data(id, data)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::ExtendedData { ext, data })) => { - self.extended_data(id, ext, data)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::Eof)) => { - self.eof(id)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::Close)) => { - self.close(id)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::Success)) => { - self.channel_success(id)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::Failure)) => { - self.channel_failure(id)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::XonXoff { client_can_do })) => { - self.xon_xoff_request(id, client_can_do)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::ExitStatus { exit_status })) => { - self.exit_status_request(id, exit_status)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::ExitSignal { signal_name, core_dumped, error_message, lang_tag })) => { - self.exit_signal_request(id, signal_name, core_dumped, &error_message, &lang_tag)?; - processed_count += 1; - } - Ok(Msg::Channel(id, ChannelMsg::WindowAdjusted { new_size })) => { - debug!("window adjusted to {new_size:?} for channel {id:?}"); - processed_count += 1; - } - Ok(Msg::ChannelOpenAgent { channel_ref }) => { - let id = self.channel_open_agent()?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::ChannelOpenSession { channel_ref }) => { - let id = self.channel_open_session()?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::ChannelOpenDirectTcpIp { host_to_connect, port_to_connect, originator_address, originator_port, channel_ref }) => { - let id = self.channel_open_direct_tcpip(&host_to_connect, port_to_connect, &originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::ChannelOpenDirectStreamLocal { socket_path, channel_ref }) => { - let id = self.channel_open_direct_streamlocal(&socket_path)?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::ChannelOpenForwardedTcpIp { connected_address, connected_port, originator_address, originator_port, channel_ref }) => { - let id = self.channel_open_forwarded_tcpip(&connected_address, connected_port, &originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::ChannelOpenForwardedStreamLocal { server_socket_path, channel_ref }) => { - let id = self.channel_open_forwarded_streamlocal(&server_socket_path)?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::ChannelOpenX11 { originator_address, originator_port, channel_ref }) => { - let id = self.channel_open_x11(&originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - processed_count += 1; - } - Ok(Msg::TcpIpForward { address, port, reply_channel }) => { - self.tcpip_forward(&address, port, reply_channel)?; - processed_count += 1; - } - Ok(Msg::CancelTcpIpForward { address, port, reply_channel }) => { - self.cancel_tcpip_forward(&address, port, reply_channel)?; - processed_count += 1; - } - Ok(Msg::Disconnect { reason, description, language_tag }) => { - self.common.disconnect(reason, &description, &language_tag)?; - processed_count += 1; - } - Ok(_) => { - // should be unreachable - processed_count += 1; - } - Err(TryRecvError::Empty) => { - // No more pending messages, proceed to select! - break; - } - Err(TryRecvError::Disconnected) => { - debug!("receiver disconnected"); - break; - } - } - } - // Only flush if we actually processed messages - if processed_count > 0 { - self.flush()?; - map_err!( - self.common - .packet_writer - .flush_into(&mut stream_write) - .await - )?; - } - } - - tokio::select! { - r = &mut reading => { - let (stream_read, mut buffer, mut opening_cipher) = match r { - Ok((_, stream_read, buffer, opening_cipher)) => (stream_read, buffer, opening_cipher), - Err(e) => return Err(e.into()) - }; - if buffer.buffer.len() < 5 { - is_reading = Some((stream_read, buffer, opening_cipher)); - break - } - - let mut pkt = self.maybe_decompress(&buffer)?; - - match pkt.buffer.first() { - None => (), - Some(&crate::msg::DISCONNECT) => { - debug!("break"); - is_reading = Some((stream_read, buffer, opening_cipher)); - break; - } - Some(_) => { - self.common.received_data = true; - // TODO it'd be cleaner to just pass cipher to reply() - std::mem::swap(&mut opening_cipher, &mut self.common.remote_to_local); - - match reply(&mut self, &mut handler, &mut pkt).await { - Ok(_) => {}, - Err(e) => return Err(e), - } - buffer.seqn = pkt.seqn; // TODO reply changes seqn internall, find cleaner way - - std::mem::swap(&mut opening_cipher, &mut self.common.remote_to_local); - } - } - reading.set(start_reading(stream_read, buffer, opening_cipher)); - } - () = &mut keepalive_timer => { - self.common.alive_timeouts = self.common.alive_timeouts.saturating_add(1); - if self.common.config.keepalive_max != 0 && self.common.alive_timeouts > self.common.config.keepalive_max { - debug!("Timeout, client not responding to keepalives"); - return Err(crate::Error::KeepaliveTimeout.into()); - } - sent_keepalive = true; - self.keepalive_request()?; - } - () = &mut inactivity_timer => { - debug!("timeout"); - return Err(crate::Error::InactivityTimeout.into()); - } - msg = self.receiver.recv(), if !self.kex.active() => { - match msg { - Some(Msg::Channel(id, ChannelMsg::Data { data })) => { - self.data(id, data)?; - } - Some(Msg::Channel(id, ChannelMsg::ExtendedData { ext, data })) => { - self.extended_data(id, ext, data)?; - } - Some(Msg::Channel(id, ChannelMsg::Eof)) => { - self.eof(id)?; - } - Some(Msg::Channel(id, ChannelMsg::Close)) => { - self.close(id)?; - } - Some(Msg::Channel(id, ChannelMsg::Success)) => { - self.channel_success(id)?; - } - Some(Msg::Channel(id, ChannelMsg::Failure)) => { - self.channel_failure(id)?; - } - Some(Msg::Channel(id, ChannelMsg::XonXoff { client_can_do })) => { - self.xon_xoff_request(id, client_can_do)?; - } - Some(Msg::Channel(id, ChannelMsg::ExitStatus { exit_status })) => { - self.exit_status_request(id, exit_status)?; - } - Some(Msg::Channel(id, ChannelMsg::ExitSignal { signal_name, core_dumped, error_message, lang_tag })) => { - self.exit_signal_request(id, signal_name, core_dumped, &error_message, &lang_tag)?; - } - Some(Msg::Channel(id, ChannelMsg::WindowAdjusted { new_size })) => { - debug!("window adjusted to {new_size:?} for channel {id:?}"); - } - Some(Msg::ChannelOpenAgent { channel_ref }) => { - let id = self.channel_open_agent()?; - self.channels.insert(id, channel_ref); - } - Some(Msg::ChannelOpenSession { channel_ref }) => { - let id = self.channel_open_session()?; - self.channels.insert(id, channel_ref); - } - Some(Msg::ChannelOpenDirectTcpIp { host_to_connect, port_to_connect, originator_address, originator_port, channel_ref }) => { - let id = self.channel_open_direct_tcpip(&host_to_connect, port_to_connect, &originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - } - Some(Msg::ChannelOpenDirectStreamLocal { socket_path, channel_ref }) => { - let id = self.channel_open_direct_streamlocal(&socket_path)?; - self.channels.insert(id, channel_ref); - } - Some(Msg::ChannelOpenForwardedTcpIp { connected_address, connected_port, originator_address, originator_port, channel_ref }) => { - let id = self.channel_open_forwarded_tcpip(&connected_address, connected_port, &originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - } - Some(Msg::ChannelOpenForwardedStreamLocal { server_socket_path, channel_ref }) => { - let id = self.channel_open_forwarded_streamlocal(&server_socket_path)?; - self.channels.insert(id, channel_ref); - } - Some(Msg::ChannelOpenX11 { originator_address, originator_port, channel_ref }) => { - let id = self.channel_open_x11(&originator_address, originator_port)?; - self.channels.insert(id, channel_ref); - } - Some(Msg::TcpIpForward { address, port, reply_channel }) => { - self.tcpip_forward(&address, port, reply_channel)?; - } - Some(Msg::CancelTcpIpForward { address, port, reply_channel }) => { - self.cancel_tcpip_forward(&address, port, reply_channel)?; - } - Some(Msg::Disconnect {reason, description, language_tag}) => { - self.common.disconnect(reason, &description, &language_tag)?; - } - Some(_) => { - // should be unreachable, since the receiver only gets - // messages from methods implemented within russh - unimplemented!("unimplemented (client-only?) message: {:?}", msg) - } - None => { - debug!("self.receiver: received None"); - } - } - } - } - self.flush()?; - - map_err!( - self.common - .packet_writer - .flush_into(&mut stream_write) - .await - )?; - - if self.common.received_data { - // Reset the number of failed keepalive attempts. We don't - // bother detecting keepalive response messages specifically - // (OpenSSH_9.6p1 responds with REQUEST_FAILURE aka 82). Instead - // we assume that the client is still alive if we receive any - // data from it. - self.common.alive_timeouts = 0; - } - if self.common.received_data || sent_keepalive { - if let (futures::future::Either::Right(ref mut sleep), Some(d)) = ( - keepalive_timer.as_mut().as_pin_mut(), - self.common.config.keepalive_interval, - ) { - sleep.as_mut().reset(tokio::time::Instant::now() + d); - } - } - if !sent_keepalive { - if let (futures::future::Either::Right(ref mut sleep), Some(d)) = ( - inactivity_timer.as_mut().as_pin_mut(), - self.common.config.inactivity_timeout, - ) { - sleep.as_mut().reset(tokio::time::Instant::now() + d); - } - } - } - debug!("disconnected"); - // Shutdown - map_err!(stream_write.shutdown().await)?; - loop { - if let Some((stream_read, buffer, opening_cipher)) = is_reading.take() { - reading.set(start_reading(stream_read, buffer, opening_cipher)); - } - match (&mut reading).await { - Ok((0, _, _, _)) => break, - Ok((_, r, b, opening_cipher)) => { - is_reading = Some((r, b, opening_cipher)); - } - // at this stage of session shutdown, EOF is not unexpected - Err(Error::IO(ref e)) if e.kind() == ErrorKind::UnexpectedEof => break, - Err(e) => return Err(e.into()), - } - } - - Ok(()) - } - - /// Get a handle to this session. - pub fn handle(&self) -> Handle { - self.sender.clone() - } - - pub fn writable_packet_size(&self, channel: &ChannelId) -> u32 { - if let Some(ref enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(channel) { - return channel - .sender_window_size - .min(channel.sender_maximum_packet_size); - } - } - 0 - } - - pub fn window_size(&self, channel: &ChannelId) -> u32 { - if let Some(ref enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(channel) { - return channel.sender_window_size; - } - } - 0 - } - - pub fn max_packet_size(&self, channel: &ChannelId) -> u32 { - if let Some(ref enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(channel) { - return channel.sender_maximum_packet_size; - } - } - 0 - } - - /// Flush the session, i.e. encrypt the pending buffer. - pub fn flush(&mut self) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - if enc.flush( - &self.common.config.as_ref().limits, - &mut self.common.packet_writer, - )? && self.kex == SessionKexState::Idle - { - debug!("starting rekeying"); - if enc.exchange.take().is_some() { - self.begin_rekey()?; - } - } - } - Ok(()) - } - - pub fn flush_pending(&mut self, channel: ChannelId) -> Result { - if let Some(ref mut enc) = self.common.encrypted { - enc.flush_pending(channel) - } else { - Ok(0) - } - } - - pub fn sender_window_size(&self, channel: ChannelId) -> usize { - if let Some(ref enc) = self.common.encrypted { - enc.sender_window_size(channel) - } else { - 0 - } - } - - pub fn has_pending_data(&self, channel: ChannelId) -> bool { - if let Some(ref enc) = self.common.encrypted { - enc.has_pending_data(channel) - } else { - false - } - } - - /// Retrieves the configuration of this session. - pub fn config(&self) -> &Config { - &self.common.config - } - - /// Sends a disconnect message. - pub fn disconnect( - &mut self, - reason: Disconnect, - description: &str, - language_tag: &str, - ) -> Result<(), Error> { - self.common.disconnect(reason, description, language_tag) - } - - /// Sends a debug message to the client. - /// - /// Debug messages are intended for debugging purposes and may be - /// optionally displayed by the client, depending on the - /// `always_display` flag and client configuration. - /// - /// # Parameters - /// - /// - `always_display`: If `true`, the client is encouraged to - /// display the message regardless of user preferences. - /// - `message`: The debug message to be sent. - /// - `language_tag`: The language tag of the message. - /// - /// # Notes - /// - /// This message is informational and does not affect the SSH session - /// state. Most clients (e.g., OpenSSH) will only display the message - /// if verbose mode is enabled. - pub fn debug( - &mut self, - always_display: bool, - message: &str, - language_tag: &str, - ) -> Result<(), Error> { - self.common.debug(always_display, message, language_tag) - } - - /// Send a "success" reply to a /global/ request (requests without - /// a channel number, such as TCP/IP forwarding or - /// cancelling). Always call this function if the request was - /// successful (it checks whether the client expects an answer). - pub fn request_success(&mut self) { - if self.common.wants_reply { - if let Some(ref mut enc) = self.common.encrypted { - self.common.wants_reply = false; - push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) - } - } - } - - /// Send a "failure" reply to a global request. - pub fn request_failure(&mut self) { - if let Some(ref mut enc) = self.common.encrypted { - self.common.wants_reply = false; - push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) - } - } - - /// Send a "success" reply to a channel request. Always call this - /// function if the request was successful (it checks whether the - /// client expects an answer). - pub fn channel_success(&mut self, channel: ChannelId) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get_mut(&channel) { - assert!(channel.confirmed); - if channel.wants_reply { - channel.wants_reply = false; - debug!("channel_success {channel:?}"); - push_packet!(enc.write, { - msg::CHANNEL_SUCCESS.encode(&mut enc.write)?; - channel.recipient_channel.encode(&mut enc.write)?; - }) - } - } - } - Ok(()) - } - - /// Send a "failure" reply to a global request. - pub fn channel_failure(&mut self, channel: ChannelId) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get_mut(&channel) { - assert!(channel.confirmed); - if channel.wants_reply { - channel.wants_reply = false; - push_packet!(enc.write, { - enc.write.push(msg::CHANNEL_FAILURE); - channel.recipient_channel.encode(&mut enc.write)?; - }) - } - } - } - Ok(()) - } - - /// Send a "failure" reply to a request to open a channel open. - pub fn channel_open_failure( - &mut self, - channel: ChannelId, - reason: ChannelOpenFailure, - description: &str, - language: &str, - ) -> Result<(), crate::Error> { - if let Some(ref mut enc) = self.common.encrypted { - push_packet!(enc.write, { - enc.write.push(msg::CHANNEL_OPEN_FAILURE); - channel.encode(&mut enc.write)?; - (reason as u32).encode(&mut enc.write)?; - description.encode(&mut enc.write)?; - language.encode(&mut enc.write)?; - }) - } - Ok(()) - } - - /// Close a channel. - pub fn close(&mut self, channel: ChannelId) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - enc.close(channel) - } else { - unreachable!() - } - } - - /// Send EOF to a channel - pub fn eof(&mut self, channel: ChannelId) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - enc.eof(channel) - } else { - unreachable!() - } - } - - /// Send data to a channel. On session channels, `extended` can be - /// used to encode standard error by passing `Some(1)`, and stdout - /// by passing `None`. - /// - /// The number of bytes added to the "sending pipeline" (to be - /// processed by the event loop) is returned. - pub fn data(&mut self, channel: ChannelId, data: impl Into) -> Result<(), Error> { - let is_rekeying = self.kex.active(); - let common = &mut self.common; - if let Some(enc) = common.encrypted.as_mut() { - enc.data_with_writer(&mut common.packet_writer, channel, data, is_rekeying) - } else { - unreachable!() - } - } - - /// Send data to a channel. On session channels, `extended` can be - /// used to encode standard error by passing `Some(1)`, and stdout - /// by passing `None`. - /// - /// The number of bytes added to the "sending pipeline" (to be - /// processed by the event loop) is returned. - pub fn extended_data( - &mut self, - channel: ChannelId, - extended: u32, - data: impl Into, - ) -> Result<(), Error> { - let is_rekeying = self.kex.active(); - let common = &mut self.common; - if let Some(enc) = common.encrypted.as_mut() { - enc.extended_data_with_writer( - &mut common.packet_writer, - channel, - extended, - data, - is_rekeying, - ) - } else { - unreachable!() - } - } - - /// Inform the client of whether they may perform - /// control-S/control-Q flow control. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.8). - pub fn xon_xoff_request( - &mut self, - channel: ChannelId, - client_can_do: bool, - ) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - assert!(channel.confirmed); - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "xon-xoff".encode(&mut enc.write)?; - 0u8.encode(&mut enc.write)?; - (client_can_do as u8).encode(&mut enc.write)?; - }) - } - } - Ok(()) - } - - /// Ping the client to verify there is still connectivity. - pub fn keepalive_request(&mut self) -> Result<(), Error> { - let want_reply = u8::from(true); - if let Some(ref mut enc) = self.common.encrypted { - self.open_global_requests - .push_back(GlobalRequestResponse::Keepalive); - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "keepalive@openssh.com".encode(&mut enc.write)?; - want_reply.encode(&mut enc.write)?; - }) - } - Ok(()) - } - - /// Ping the client with a Keepalive and get a notification when the client responds. - pub fn send_ping(&mut self, reply_channel: oneshot::Sender<()>) -> Result<(), Error> { - let want_reply = u8::from(true); - if let Some(ref mut enc) = self.common.encrypted { - self.open_global_requests - .push_back(GlobalRequestResponse::Ping(reply_channel)); - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "keepalive@openssh.com".encode(&mut enc.write)?; - want_reply.encode(&mut enc.write)?; - }) - } - Ok(()) - } - - /// Send the exit status of a program. - pub fn exit_status_request( - &mut self, - channel: ChannelId, - exit_status: u32, - ) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - assert!(channel.confirmed); - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "exit-status".encode(&mut enc.write)?; - 0u8.encode(&mut enc.write)?; - exit_status.encode(&mut enc.write)?; - }) - } - } - Ok(()) - } - - /// If the program was killed by a signal, send the details about the signal to the client. - pub fn exit_signal_request( - &mut self, - channel: ChannelId, - signal: Sig, - core_dumped: bool, - error_message: &str, - language_tag: &str, - ) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - if let Some(channel) = enc.channels.get(&channel) { - assert!(channel.confirmed); - push_packet!(enc.write, { - msg::CHANNEL_REQUEST.encode(&mut enc.write)?; - - channel.recipient_channel.encode(&mut enc.write)?; - "exit-signal".encode(&mut enc.write)?; - 0u8.encode(&mut enc.write)?; - signal.name().encode(&mut enc.write)?; - (core_dumped as u8).encode(&mut enc.write)?; - error_message.encode(&mut enc.write)?; - language_tag.encode(&mut enc.write)?; - }) - } - } - Ok(()) - } - - /// Opens a new session channel on the client. - pub fn channel_open_session(&mut self) -> Result { - self.channel_open_generic(b"session", |_| Ok(())) - } - - /// Opens a direct-tcpip channel on the client (non-standard). - pub fn channel_open_direct_tcpip( - &mut self, - host_to_connect: &str, - port_to_connect: u32, - originator_address: &str, - originator_port: u32, - ) -> Result { - self.channel_open_generic(b"direct-tcpip", |write| { - host_to_connect.encode(write)?; - port_to_connect.encode(write)?; // sender channel id. - originator_address.encode(write)?; - originator_port.encode(write)?; // sender channel id. - Ok(()) - }) - } - - /// Opens a direct-streamlocal channel on the client (non-standard). - pub fn channel_open_direct_streamlocal( - &mut self, - socket_path: &str, - ) -> Result { - self.channel_open_generic(b"direct-streamlocal@openssh.com", |write| { - socket_path.encode(write)?; - "".encode(write)?; // reserved - 0u32.encode(write)?; // reserved - Ok(()) - }) - } - - /// Open a TCP/IP forwarding channel, when a connection comes to a - /// local port for which forwarding has been requested. See - /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). The - /// TCP/IP packets can then be tunneled through the channel using - /// `.data()`. - pub fn channel_open_forwarded_tcpip( - &mut self, - connected_address: &str, - connected_port: u32, - originator_address: &str, - originator_port: u32, - ) -> Result { - self.channel_open_generic(b"forwarded-tcpip", |write| { - connected_address.encode(write)?; - connected_port.encode(write)?; // sender channel id. - originator_address.encode(write)?; - originator_port.encode(write)?; // sender channel id. - Ok(()) - }) - } - - pub fn channel_open_forwarded_streamlocal( - &mut self, - socket_path: &str, - ) -> Result { - self.channel_open_generic(b"forwarded-streamlocal@openssh.com", |write| { - socket_path.encode(write)?; - "".encode(write)?; - Ok(()) - }) - } - - /// Open a new X11 channel, when a connection comes to a - /// local port. See [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.3.2). - /// TCP/IP packets can then be tunneled through the channel using `.data()`. - pub fn channel_open_x11( - &mut self, - originator_address: &str, - originator_port: u32, - ) -> Result { - self.channel_open_generic(b"x11", |write| { - originator_address.encode(write)?; - originator_port.encode(write)?; - Ok(()) - }) - } - - /// Opens a new agent channel on the client. - pub fn channel_open_agent(&mut self) -> Result { - self.channel_open_generic(b"auth-agent@openssh.com", |_| Ok(())) - } - - fn channel_open_generic(&mut self, kind: &[u8], write_suffix: F) -> Result - where - F: FnOnce(&mut Vec) -> Result<(), Error>, - { - let result = if let Some(ref mut enc) = self.common.encrypted { - if !matches!( - enc.state, - EncryptedState::Authenticated | EncryptedState::InitCompression - ) { - return Err(Error::Inconsistent); - } - - let sender_channel = enc.new_channel( - self.common.config.window_size, - self.common.config.maximum_packet_size, - ); - push_packet!(enc.write, { - enc.write.push(msg::CHANNEL_OPEN); - kind.encode(&mut enc.write)?; - - // sender channel id. - sender_channel.encode(&mut enc.write)?; - - // window. - self.common - .config - .as_ref() - .window_size - .encode(&mut enc.write)?; - - // max packet size. - self.common - .config - .as_ref() - .maximum_packet_size - .encode(&mut enc.write)?; - - write_suffix(&mut enc.write)?; - }); - sender_channel - } else { - return Err(Error::Inconsistent); - }; - Ok(result) - } - - /// Requests that the client forward connections to the given host and port. - /// See [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). The client - /// will open forwarded_tcpip channels for each connection. - pub fn tcpip_forward( - &mut self, - address: &str, - port: u32, - reply_channel: Option>>, - ) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - let want_reply = reply_channel.is_some(); - if let Some(reply_channel) = reply_channel { - self.open_global_requests.push_back( - crate::session::GlobalRequestResponse::TcpIpForward(reply_channel), - ); - } - push_packet!(enc.write, { - enc.write.push(msg::GLOBAL_REQUEST); - "tcpip-forward".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - address.encode(&mut enc.write)?; - port.encode(&mut enc.write)?; - }); - } - Ok(()) - } - - /// Cancels a previously tcpip_forward request. - pub fn cancel_tcpip_forward( - &mut self, - address: &str, - port: u32, - reply_channel: Option>, - ) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - let want_reply = reply_channel.is_some(); - if let Some(reply_channel) = reply_channel { - self.open_global_requests.push_back( - crate::session::GlobalRequestResponse::CancelTcpIpForward(reply_channel), - ); - } - push_packet!(enc.write, { - msg::GLOBAL_REQUEST.encode(&mut enc.write)?; - "cancel-tcpip-forward".encode(&mut enc.write)?; - (want_reply as u8).encode(&mut enc.write)?; - address.encode(&mut enc.write)?; - port.encode(&mut enc.write)?; - }); - } - Ok(()) - } - - /// Returns the SSH ID (Protocol Version + Software Version) the client sent when connecting - /// - /// This should contain only ASCII characters for implementations conforming to RFC4253, Section 4.2: - /// - /// > Both the 'protoversion' and 'softwareversion' strings MUST consist of - /// > printable US-ASCII characters, with the exception of whitespace - /// > characters and the minus sign (-). - /// - /// So it usually is fine to convert it to a [`String`] using [`String::from_utf8_lossy`] - pub fn remote_sshid(&self) -> &[u8] { - &self.common.remote_sshid - } - - pub(crate) fn maybe_send_ext_info(&mut self) -> Result<(), Error> { - if let Some(ref mut enc) = self.common.encrypted { - // If client sent a ext-info-c message in the kex list, it supports RFC 8308 extension negotiation. - let mut key_extension_client = false; - if let Some(e) = &enc.exchange { - let Some(mut r) = e.client_kex_init.get(17..) else { - return Ok(()); - }; - if let Ok(kex_list) = NameList::decode(&mut r) { - use super::negotiation::Select; - key_extension_client = super::negotiation::Server::select( - &[EXTENSION_SUPPORT_AS_CLIENT], - &kex_list, - AlgorithmKind::Kex, - ) - .is_ok(); - } - } - - if !key_extension_client { - debug!("RFC 8308 Extension Negotiation not supported by client"); - return Ok(()); - } - - push_packet!(enc.write, { - msg::EXT_INFO.encode(&mut enc.write)?; - 1u32.encode(&mut enc.write)?; - "server-sig-algs".encode(&mut enc.write)?; - - NameList( - self.common - .config - .preferred - .key - .iter() - .map(|x| x.to_string()) - .collect(), - ) - .encode(&mut enc.write)?; - }); - } - Ok(()) - } - - pub(crate) fn begin_rekey(&mut self) -> Result<(), Error> { - debug!("beginning re-key"); - let mut kex = ServerKex::new( - self.common.config.clone(), - &self.common.remote_sshid, - &self.common.config.server_id, - match self.common.encrypted { - None => KexCause::Initial, - Some(ref enc) => KexCause::Rekey { - strict: self.common.strict_kex, - session_id: enc.session_id.clone(), - }, - }, - ); - - kex.kexinit(&mut self.common.packet_writer)?; - self.kex = SessionKexState::InProgress(kex); - Ok(()) - } -} - -#[cfg(all(test, feature = "flate2"))] -mod tests { - use std::collections::{HashMap, VecDeque}; - use std::io::Write; - use std::num::Wrapping; - use std::sync::Arc; - - use super::*; - use crate::compression::{Compression, Decompress}; - use crate::kex::{KEXES, NONE, SessionKexState}; - use crate::session::{CommonSession, Encrypted, EncryptedState, Exchange}; - use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer}; - use crate::{CryptoVec, cipher, mac}; - - struct TestHandler; - - impl crate::server::Handler for TestHandler { - type Error = crate::Error; - } - - fn authenticated_session() -> Session { - let config = Arc::new(crate::server::Config::default()); - let (sender, receiver) = tokio::sync::mpsc::channel(config.event_buffer_size); - let handle = Handle { - sender, - channel_buffer_size: config.channel_buffer_size, - }; - - Session { - common: CommonSession { - auth_user: String::new(), - remote_sshid: b"SSH-2.0-test".to_vec(), - config: config.clone(), - encrypted: Some(Encrypted { - state: EncryptedState::Authenticated, - exchange: Some(Exchange::default()), - kex: KEXES.get(&NONE).unwrap().make(), - key: 0, - client_mac: mac::NONE, - server_mac: mac::NONE, - session_id: CryptoVec::new(), - channels: HashMap::new(), - last_channel_id: Wrapping(0), - write: Vec::new(), - write_cursor: 0, - last_rekey: russh_util::time::Instant::now(), - server_compression: Compression::None, - client_compression: Compression::None, - decompress: Decompress::Zlib(flate2::Decompress::new(true)), - rekey_wanted: false, - received_extensions: Vec::new(), - extension_info_awaiters: HashMap::new(), - }), - auth_method: None, - auth_attempts: 0, - packet_writer: PacketWriter::clear(), - remote_to_local: Box::new(cipher::clear::Key), - wants_reply: false, - disconnected: false, - buffer: Vec::new(), - strict_kex: false, - alive_timeouts: 0, - received_data: false, - }, - sender: handle, - receiver, - target_window_size: config.window_size, - pending_reads: Vec::new(), - pending_len: 0, - channels: HashMap::new(), - open_global_requests: VecDeque::new(), - kex: SessionKexState::Idle, - } - } - - fn compressed_debug_payload(payload_len: usize) -> Vec { - let mut payload = vec![b'A'; payload_len]; - payload[0] = crate::msg::DEBUG; - - let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best()); - encoder.write_all(&payload).unwrap(); - let compressed = encoder.finish().unwrap(); - assert!(compressed.len() < 256 * 1024); - compressed - } - - fn incoming_packet(compressed: Vec) -> SSHBuffer { - let mut buffer = SSHBuffer::new(); - buffer.buffer.extend_from_slice(&[0; 5]); - buffer.buffer.extend_from_slice(&compressed); - buffer - } - - #[tokio::test] - async fn compressed_debug_is_ignored_after_server_parses_it() { - let mut session = authenticated_session(); - let mut handler = TestHandler; - let buffer = incoming_packet(compressed_debug_payload(200 * 1024)); - let mut pkt: IncomingSshPacket = session.maybe_decompress(&buffer).unwrap(); - - super::super::reply(&mut session, &mut handler, &mut pkt) - .await - .unwrap(); - - assert!(!session.common.disconnected); - } - - #[test] - fn oversized_compressed_debug_is_rejected_before_server_ignores_it() { - let mut session = authenticated_session(); - let buffer = incoming_packet(compressed_debug_payload( - crate::cipher::MAXIMUM_DECOMPRESSED_PACKET_LEN + 1024, - )); - - let err = session.maybe_decompress(&buffer).unwrap_err(); - assert!( - matches!(err, crate::Error::PacketSize(len) if len > crate::cipher::MAXIMUM_DECOMPRESSED_PACKET_LEN) - ); - } -} diff --git a/crates/bssh-russh/src/session.rs b/crates/bssh-russh/src/session.rs deleted file mode 100644 index b1eae0be..00000000 --- a/crates/bssh-russh/src/session.rs +++ /dev/null @@ -1,1468 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; -use std::mem::replace; -use std::num::Wrapping; - -use bytes::Bytes; -use byteorder::{BigEndian, ByteOrder}; -use log::{debug, trace}; -use ssh_encoding::Encode; -use tokio::sync::oneshot; - -use crate::cipher::OpeningKey; -use crate::client::GexParams; -use crate::kex::dh::groups::DhGroup; -use crate::kex::{KexAlgorithm, KexAlgorithmImplementor}; -use crate::sshbuffer::PacketWriter; -use crate::{ - ChannelId, ChannelParams, CryptoVec, Disconnect, Limits, auth, cipher, mac, msg, negotiation, -}; - -#[derive(Debug)] -pub(crate) struct Encrypted { - pub state: EncryptedState, - - // It's always Some, except when we std::mem::replace it temporarily. - pub exchange: Option, - pub kex: KexAlgorithm, - pub key: usize, - pub client_mac: mac::Name, - pub server_mac: mac::Name, - pub session_id: CryptoVec, - pub channels: HashMap, - pub last_channel_id: Wrapping, - // Non-sensitive packet assembly buffer, analogous to - // OpenSSH sshbuf (output side). Not mlocked because it - // holds only protocol framing and ciphertext. - pub write: Vec, - pub write_cursor: usize, - pub last_rekey: russh_util::time::Instant, - pub server_compression: crate::compression::Compression, - pub client_compression: crate::compression::Compression, - pub decompress: crate::compression::Decompress, - pub rekey_wanted: bool, - pub received_extensions: Vec, - pub extension_info_awaiters: HashMap>>, -} - -pub(crate) struct CommonSession { - pub auth_user: String, - pub remote_sshid: Vec, - pub config: Config, - pub encrypted: Option, - pub auth_method: Option, - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] - pub(crate) auth_attempts: usize, - pub packet_writer: PacketWriter, - pub remote_to_local: Box, - pub wants_reply: bool, - pub disconnected: bool, - // Non-sensitive incoming-packet scratch buffer. - pub buffer: Vec, - pub strict_kex: bool, - pub alive_timeouts: usize, - pub received_data: bool, -} - -impl Debug for CommonSession { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CommonSession") - .field("auth_user", &self.auth_user) - .field("remote_sshid", &self.remote_sshid) - .field("encrypted", &self.encrypted) - .field("auth_method", &self.auth_method) - .field("auth_attempts", &self.auth_attempts) - .field("packet_writer", &self.packet_writer) - .field("wants_reply", &self.wants_reply) - .field("disconnected", &self.disconnected) - .field("buffer", &self.buffer) - .field("strict_kex", &self.strict_kex) - .field("alive_timeouts", &self.alive_timeouts) - .field("received_data", &self.received_data) - .finish() - } -} - -#[must_use] -#[derive(Debug, Clone, Copy)] -pub(crate) enum ChannelFlushResult { - Incomplete { - wrote: usize, - }, - Complete { - wrote: usize, - pending_eof: bool, - pending_close: bool, - }, -} -impl ChannelFlushResult { - pub(crate) fn wrote(&self) -> usize { - match self { - ChannelFlushResult::Incomplete { wrote } => *wrote, - ChannelFlushResult::Complete { wrote, .. } => *wrote, - } - } - pub(crate) fn complete(wrote: usize, channel: &mut ChannelParams) -> Self { - let (pending_eof, pending_close) = channel.take_pending_controls(); - ChannelFlushResult::Complete { - wrote, - pending_eof, - pending_close, - } - } -} - -impl CommonSession { - pub fn newkeys(&mut self, newkeys: NewKeys) { - if let Some(ref mut enc) = self.encrypted { - enc.exchange = Some(newkeys.exchange); - enc.kex = newkeys.kex; - enc.key = newkeys.key; - enc.client_mac = newkeys.names.client_mac; - enc.server_mac = newkeys.names.server_mac; - self.remote_to_local = newkeys.cipher.remote_to_local; - self.packet_writer - .set_cipher(newkeys.cipher.local_to_remote); - self.strict_kex = self.strict_kex || newkeys.names.strict_kex(); - - // Reset compression state - enc.client_compression - .init_compress(self.packet_writer.compress()); - enc.server_compression.init_decompress(&mut enc.decompress); - } - } - - pub fn encrypted(&mut self, state: EncryptedState, newkeys: NewKeys) { - let strict_kex = newkeys.names.strict_kex(); - self.encrypted = Some(Encrypted { - exchange: Some(newkeys.exchange), - kex: newkeys.kex, - key: newkeys.key, - client_mac: newkeys.names.client_mac, - server_mac: newkeys.names.server_mac, - session_id: newkeys.session_id, - state, - channels: HashMap::new(), - last_channel_id: Wrapping(1), - write: Vec::new(), - write_cursor: 0, - last_rekey: russh_util::time::Instant::now(), - server_compression: newkeys.names.server_compression, - client_compression: newkeys.names.client_compression, - decompress: crate::compression::Decompress::None, - rekey_wanted: false, - received_extensions: Vec::new(), - extension_info_awaiters: HashMap::new(), - }); - self.remote_to_local = newkeys.cipher.remote_to_local; - self.packet_writer - .set_cipher(newkeys.cipher.local_to_remote); - self.strict_kex = strict_kex; - - // For non-deferred compression (RFC 4253 "zlib"), activate immediately - // after initial key exchange. Deferred compression ("zlib@openssh.com") - // will be activated later, after authentication succeeds. - if let Some(ref mut enc) = self.encrypted { - if !enc.client_compression.is_deferred() { - enc.client_compression - .init_compress(self.packet_writer.compress()); - } - if !enc.server_compression.is_deferred() { - enc.server_compression - .init_decompress(&mut enc.decompress); - } - } - } - - /// Send a disconnect message. - pub fn disconnect( - &mut self, - reason: Disconnect, - description: &str, - language_tag: &str, - ) -> Result<(), crate::Error> { - let disconnect = |buf: &mut Vec| { - push_packet!(buf, { - msg::DISCONNECT.encode(buf)?; - (reason as u32).encode(buf)?; - description.encode(buf)?; - language_tag.encode(buf)?; - }); - Ok(()) - }; - if !self.disconnected { - self.disconnected = true; - return if let Some(ref mut enc) = self.encrypted { - disconnect(&mut enc.write) - } else { - disconnect(&mut self.packet_writer.buffer().buffer) - }; - } - Ok(()) - } - - /// Send a debug message. - pub fn debug( - &mut self, - always_display: bool, - message: &str, - language_tag: &str, - ) -> Result<(), crate::Error> { - let debug = |buf: &mut Vec| { - push_packet!(buf, { - msg::DEBUG.encode(buf)?; - (always_display as u8).encode(buf)?; - message.encode(buf)?; - language_tag.encode(buf)?; - }); - Ok(()) - }; - if let Some(ref mut enc) = self.encrypted { - debug(&mut enc.write) - } else { - debug(&mut self.packet_writer.buffer().buffer) - } - } - - pub(crate) fn reset_seqn(&mut self) { - self.packet_writer.reset_seqn(); - } -} - -impl Encrypted { - pub fn byte(&mut self, channel: ChannelId, msg: u8) -> Result<(), crate::Error> { - if let Some(channel) = self.channels.get(&channel) { - push_packet!(self.write, { - self.write.push(msg); - channel.recipient_channel.encode(&mut self.write)?; - }); - } - Ok(()) - } - - pub fn eof(&mut self, channel: ChannelId) -> Result<(), crate::Error> { - if let Some(channel) = self.has_pending_data_mut(channel) { - channel.pending_eof = true; - } else { - self.byte(channel, msg::CHANNEL_EOF)?; - } - Ok(()) - } - - pub fn close(&mut self, channel: ChannelId) -> Result<(), crate::Error> { - if let Some(channel) = self.has_pending_data_mut(channel) { - channel.pending_close = true; - } else { - self.byte(channel, msg::CHANNEL_CLOSE)?; - self.channels.remove(&channel); - } - Ok(()) - } - - pub fn sender_window_size(&self, channel: ChannelId) -> usize { - if let Some(channel) = self.channels.get(&channel) { - channel.sender_window_size as usize - } else { - 0 - } - } - - pub fn adjust_window_size( - &mut self, - channel: ChannelId, - data: &[u8], - target: u32, - ) -> Result { - if let Some(channel) = self.channels.get_mut(&channel) { - trace!( - "adjust_window_size, channel = {}, size = {},", - channel.sender_channel, target - ); - // Ignore extra data. - // https://tools.ietf.org/html/rfc4254#section-5.2 - if data.len() as u32 <= channel.sender_window_size { - channel.sender_window_size -= data.len() as u32; - } - if channel.sender_window_size < target / 2 { - debug!( - "sender_window_size {:?}, target {:?}", - channel.sender_window_size, target - ); - push_packet!(self.write, { - self.write.push(msg::CHANNEL_WINDOW_ADJUST); - channel.recipient_channel.encode(&mut self.write)?; - (target - channel.sender_window_size).encode(&mut self.write)?; - }); - channel.sender_window_size = target; - return Ok(true); - } - } - Ok(false) - } - - fn flush_channel( - write: &mut Vec, - channel: &mut ChannelParams, - ) -> Result { - let mut pending_size = 0; - while let Some((buf, a, from)) = channel.pending_data.pop_front() { - let size = Self::data_noqueue(write, channel, &buf, a, from)?; - pending_size += size; - if from + size < buf.len() { - channel.pending_data.push_front((buf, a, from + size)); - return Ok(ChannelFlushResult::Incomplete { - wrote: pending_size, - }); - } - } - Ok(ChannelFlushResult::complete(pending_size, channel)) - } - - fn flush_channel_with_writer( - write: &mut Vec, - writer: &mut PacketWriter, - channel: &mut ChannelParams, - ) -> Result { - let mut pending_size = 0; - while let Some((buf, a, from)) = channel.pending_data.pop_front() { - let size = if write.is_empty() { - Self::data_noqueue_direct(writer, channel, &buf, a, from)? - } else { - Self::data_noqueue(write, channel, &buf, a, from)? - }; - pending_size += size; - if from + size < buf.len() { - channel.pending_data.push_front((buf, a, from + size)); - return Ok(ChannelFlushResult::Incomplete { - wrote: pending_size, - }); - } - } - Ok(ChannelFlushResult::complete(pending_size, channel)) - } - - fn handle_flushed_channel( - &mut self, - channel: ChannelId, - flush_result: ChannelFlushResult, - ) -> Result<(), crate::Error> { - if let ChannelFlushResult::Complete { - wrote: _, - pending_eof, - pending_close, - } = flush_result - { - if pending_eof { - self.eof(channel)?; - } - if pending_close { - self.close(channel)?; - } - } - Ok(()) - } - - pub fn flush_pending(&mut self, channel: ChannelId) -> Result { - let flush_result = match self.channels.get_mut(&channel) { - Some(ch) => Self::flush_channel(&mut self.write, ch)?, - None => return Ok(0), - }; - let wrote = flush_result.wrote(); - self.handle_flushed_channel(channel, flush_result)?; - Ok(wrote) - } - - pub fn flush_pending_with_writer( - &mut self, - writer: &mut PacketWriter, - channel: ChannelId, - ) -> Result { - let flush_result = match self.channels.get_mut(&channel) { - Some(ch) => Self::flush_channel_with_writer(&mut self.write, writer, ch)?, - None => return Ok(0), - }; - let wrote = flush_result.wrote(); - self.handle_flushed_channel(channel, flush_result)?; - Ok(wrote) - } - - #[allow(dead_code)] - pub fn flush_all_pending(&mut self) -> Result<(), crate::Error> { - let channel_ids: Vec = self.channels.keys().copied().collect(); - for channel_id in channel_ids { - self.flush_pending(channel_id)?; - } - Ok(()) - } - - pub fn flush_all_pending_with_writer( - &mut self, - writer: &mut PacketWriter, - ) -> Result<(), crate::Error> { - let channel_ids: Vec = self.channels.keys().copied().collect(); - for channel_id in channel_ids { - self.flush_pending_with_writer(writer, channel_id)?; - } - Ok(()) - } - - fn has_pending_data_mut(&mut self, channel: ChannelId) -> Option<&mut ChannelParams> { - self.channels - .get_mut(&channel) - .filter(|c| !c.pending_data.is_empty()) - } - - pub fn has_pending_data(&self, channel: ChannelId) -> bool { - if let Some(channel) = self.channels.get(&channel) { - !channel.pending_data.is_empty() - } else { - false - } - } - - /// Push the largest amount of `&buf0[from..]` that can fit into - /// the window, dividing it into packets if it is too large, and - /// return the length that was written. - fn data_noqueue( - write: &mut Vec, - channel: &mut ChannelParams, - buf0: &[u8], - a: Option, - from: usize, - ) -> Result { - if from >= buf0.len() { - return Ok(0); - } - let window_end = from - .checked_add(channel.recipient_window_size as usize) - .unwrap_or(usize::MAX); - let end = std::cmp::min(buf0.len(), window_end); - #[allow(clippy::indexing_slicing)] // length checked - let mut buf = &buf0[from..end]; - let buf_len = buf.len(); - let max_packet_size = channel.recipient_maximum_packet_size as usize; - if max_packet_size == 0 { - return Err(crate::Error::Inconsistent); - } - let packet_count = buf_len.div_ceil(max_packet_size); - let packet_overhead = match a { - None => 4 + 1 + 4 + 4, - Some(_) => 4 + 1 + 4 + 4 + 4, - }; - write.reserve(buf_len.saturating_add(packet_count.saturating_mul(packet_overhead))); - - while !buf.is_empty() { - // Compute the length we're allowed to send. - let off = std::cmp::min(buf.len(), max_packet_size); - match a { - None => push_packet!(write, { - write.push(msg::CHANNEL_DATA); - channel.recipient_channel.encode(write)?; - #[allow(clippy::indexing_slicing)] // length checked - buf[..off].encode(write)?; - }), - Some(ext) => push_packet!(write, { - write.push(msg::CHANNEL_EXTENDED_DATA); - channel.recipient_channel.encode(write)?; - ext.encode(write)?; - #[allow(clippy::indexing_slicing)] // length checked - buf[..off].encode(write)?; - }), - } - trace!( - "buffer: {:?} {:?}", - write.len(), - channel.recipient_window_size - ); - channel.recipient_window_size -= off as u32; - #[allow(clippy::indexing_slicing)] // length checked - { - buf = &buf[off..] - } - } - trace!("buf.len() = {:?}, buf_len = {:?}", buf.len(), buf_len); - Ok(buf_len) - } - - fn data_noqueue_direct( - writer: &mut PacketWriter, - channel: &mut ChannelParams, - buf0: &Bytes, - a: Option, - from: usize, - ) -> Result { - if from >= buf0.len() { - return Ok(0); - } - let buf0 = buf0.as_ref(); - let window_end = from - .checked_add(channel.recipient_window_size as usize) - .unwrap_or(usize::MAX); - let end = std::cmp::min(buf0.len(), window_end); - #[allow(clippy::indexing_slicing)] // length checked - let mut buf = &buf0[from..end]; - let buf_len = buf.len(); - let max_packet_size = channel.recipient_maximum_packet_size as usize; - if max_packet_size == 0 { - return Err(crate::Error::Inconsistent); - } - let packet_count = buf_len.div_ceil(max_packet_size); - let channel_payload_overhead = match a { - None => 1 + 4 + 4, - Some(_) => 1 + 4 + 4 + 4, - }; - writer.reserve_cleartext_packet_output( - buf_len.saturating_add(packet_count.saturating_mul(channel_payload_overhead)), - packet_count, - ); - - while !buf.is_empty() { - let off = std::cmp::min(buf.len(), max_packet_size); - match a { - None => writer.write_packet(|packet| { - packet.push(msg::CHANNEL_DATA); - channel.recipient_channel.encode(packet)?; - #[allow(clippy::indexing_slicing)] // length checked - buf[..off].encode(packet)?; - Ok(()) - })?, - Some(ext) => writer.write_packet(|packet| { - packet.push(msg::CHANNEL_EXTENDED_DATA); - channel.recipient_channel.encode(packet)?; - ext.encode(packet)?; - #[allow(clippy::indexing_slicing)] // length checked - buf[..off].encode(packet)?; - Ok(()) - })?, - } - channel.recipient_window_size -= off as u32; - #[allow(clippy::indexing_slicing)] // length checked - { - buf = &buf[off..] - } - } - Ok(buf_len) - } - - #[allow(dead_code)] - pub fn data( - &mut self, - channel: ChannelId, - buf0: impl Into, - is_rekeying: bool, - ) -> Result<(), crate::Error> { - let buf0 = buf0.into(); - if let Some(channel) = self.channels.get_mut(&channel) { - assert!(channel.confirmed); - if !channel.pending_data.is_empty() || is_rekeying { - channel.pending_data.push_back((buf0, None, 0)); - return Ok(()); - } - let buf_len = Self::data_noqueue(&mut self.write, channel, &buf0, None, 0)?; - if buf_len < buf0.len() { - channel.pending_data.push_back((buf0, None, buf_len)) - } - } else { - debug!("{channel:?} not saved for this session"); - } - Ok(()) - } - - pub fn data_with_writer( - &mut self, - writer: &mut PacketWriter, - channel: ChannelId, - buf0: impl Into, - is_rekeying: bool, - ) -> Result<(), crate::Error> { - let buf0 = buf0.into(); - if let Some(channel) = self.channels.get_mut(&channel) { - assert!(channel.confirmed); - if !channel.pending_data.is_empty() || is_rekeying { - channel.pending_data.push_back((buf0, None, 0)); - return Ok(()); - } - let buf_len = if self.write.is_empty() { - Self::data_noqueue_direct(writer, channel, &buf0, None, 0)? - } else { - Self::data_noqueue(&mut self.write, channel, &buf0, None, 0)? - }; - if buf_len < buf0.len() { - channel.pending_data.push_back((buf0, None, buf_len)) - } - } else { - debug!("{channel:?} not saved for this session"); - } - Ok(()) - } - - #[allow(dead_code)] - pub fn extended_data( - &mut self, - channel: ChannelId, - ext: u32, - buf0: impl Into, - is_rekeying: bool, - ) -> Result<(), crate::Error> { - let buf0 = buf0.into(); - if let Some(channel) = self.channels.get_mut(&channel) { - assert!(channel.confirmed); - if !channel.pending_data.is_empty() || is_rekeying { - channel.pending_data.push_back((buf0, Some(ext), 0)); - return Ok(()); - } - let buf_len = Self::data_noqueue(&mut self.write, channel, &buf0, Some(ext), 0)?; - if buf_len < buf0.len() { - channel.pending_data.push_back((buf0, Some(ext), buf_len)) - } - } - Ok(()) - } - - pub fn extended_data_with_writer( - &mut self, - writer: &mut PacketWriter, - channel: ChannelId, - ext: u32, - buf0: impl Into, - is_rekeying: bool, - ) -> Result<(), crate::Error> { - let buf0 = buf0.into(); - if let Some(channel) = self.channels.get_mut(&channel) { - assert!(channel.confirmed); - if !channel.pending_data.is_empty() || is_rekeying { - channel.pending_data.push_back((buf0, Some(ext), 0)); - return Ok(()); - } - let buf_len = if self.write.is_empty() { - Self::data_noqueue_direct(writer, channel, &buf0, Some(ext), 0)? - } else { - Self::data_noqueue(&mut self.write, channel, &buf0, Some(ext), 0)? - }; - if buf_len < buf0.len() { - channel.pending_data.push_back((buf0, Some(ext), buf_len)) - } - } - Ok(()) - } - - pub fn flush( - &mut self, - limits: &Limits, - writer: &mut PacketWriter, - ) -> Result { - // If there are pending packets (and we've not started to rekey), flush them. - { - while self.write_cursor < self.write.len() { - // Read a single packet, encrypt and send it. - #[allow(clippy::indexing_slicing)] // length checked - let len = BigEndian::read_u32(&self.write[self.write_cursor..]) as usize; - #[allow(clippy::indexing_slicing)] - let to_write = &self.write[(self.write_cursor + 4)..(self.write_cursor + 4 + len)]; - trace!("session_write_encrypted, buf = {to_write:?}"); - - writer.packet_raw(to_write)?; - self.write_cursor += 4 + len - } - } - if self.write_cursor >= self.write.len() { - // If all packets have been written, clear. - self.write_cursor = 0; - self.write.clear(); - } - - if self.kex.skip_exchange() { - return Ok(false); - } - - let now = russh_util::time::Instant::now(); - let dur = now.duration_since(self.last_rekey); - Ok(replace(&mut self.rekey_wanted, false) - || writer.buffer().bytes >= limits.rekey_write_limit - || dur >= limits.rekey_time_limit) - } - - pub fn new_channel_id(&mut self) -> ChannelId { - self.last_channel_id += Wrapping(1); - while self - .channels - .contains_key(&ChannelId(self.last_channel_id.0)) - { - self.last_channel_id += Wrapping(1) - } - ChannelId(self.last_channel_id.0) - } - pub fn new_channel(&mut self, window_size: u32, maxpacket: u32) -> ChannelId { - loop { - self.last_channel_id += Wrapping(1); - if let std::collections::hash_map::Entry::Vacant(vacant_entry) = - self.channels.entry(ChannelId(self.last_channel_id.0)) - { - vacant_entry.insert(ChannelParams { - recipient_channel: 0, - sender_channel: ChannelId(self.last_channel_id.0), - sender_window_size: window_size, - recipient_window_size: 0, - sender_maximum_packet_size: maxpacket, - recipient_maximum_packet_size: 0, - confirmed: false, - wants_reply: false, - pending_data: std::collections::VecDeque::new(), - pending_eof: false, - pending_close: false, - }); - return ChannelId(self.last_channel_id.0); - } - } - } -} - -#[derive(Debug)] -pub enum EncryptedState { - WaitingAuthServiceRequest { sent: bool, accepted: bool }, - WaitingAuthRequest(auth::AuthRequest), - InitCompression, - Authenticated, -} - -#[derive(Debug, Default, Clone)] -pub struct Exchange { - // All Exchange fields are public protocol values (identifiers, - // kex init payloads, ephemeral public keys) visible on the wire. - // They carry no secret material and do not require mlock. - pub client_id: Vec, - pub server_id: Vec, - pub client_kex_init: Bytes, - pub server_kex_init: Bytes, - pub client_ephemeral: Vec, - pub server_ephemeral: Vec, - pub gex: Option<(GexParams, DhGroup)>, -} - -impl Exchange { - pub fn new(client_id: &[u8], server_id: &[u8]) -> Self { - Exchange { - client_id: client_id.into(), - server_id: server_id.into(), - ..Default::default() - } - } -} - -#[derive(Debug)] -pub(crate) struct NewKeys { - pub exchange: Exchange, - pub names: negotiation::Names, - pub kex: KexAlgorithm, - pub key: usize, - pub cipher: cipher::CipherPair, - pub session_id: CryptoVec, -} - -#[derive(Debug)] -pub(crate) enum GlobalRequestResponse { - /// request was for Keepalive, ignore result - Keepalive, - /// request was for Keepalive but with notification of the result - Ping(oneshot::Sender<()>), - /// request was for NoMoreSessions, disallow additional sessions - NoMoreSessions, - /// request was for TcpIpForward, sends Some(port) for success or None for failure - TcpIpForward(oneshot::Sender>), - /// request was for CancelTcpIpForward, sends true for success or false for failure - CancelTcpIpForward(oneshot::Sender), - /// request was for StreamLocalForward, sends true for success or false for failure - StreamLocalForward(oneshot::Sender), - CancelStreamLocalForward(oneshot::Sender), -} - -#[cfg(test)] -mod tests { - use std::collections::{HashMap, VecDeque}; - use std::num::Wrapping; - - use byteorder::{BigEndian, ByteOrder}; - use bytes::Bytes; - - use super::{Encrypted, EncryptedState, Exchange}; - use crate::compression::{Compression, Decompress}; - use crate::kex::{KEXES, NONE}; - use crate::sshbuffer::PacketWriter; - use crate::{ChannelId, ChannelParams, CryptoVec, Limits, mac, msg}; - - fn test_encrypted() -> Encrypted { - Encrypted { - state: EncryptedState::Authenticated, - exchange: Some(Exchange::default()), - kex: KEXES.get(&NONE).unwrap().make(), - key: 0, - client_mac: mac::NONE, - server_mac: mac::NONE, - session_id: CryptoVec::new(), - channels: HashMap::new(), - last_channel_id: Wrapping(0), - write: Vec::new(), - write_cursor: 0, - last_rekey: russh_util::time::Instant::now(), - server_compression: Compression::None, - client_compression: Compression::None, - decompress: Decompress::None, - rekey_wanted: false, - received_extensions: Vec::new(), - extension_info_awaiters: HashMap::new(), - } - } - - fn test_channel( - sender_channel: ChannelId, - recipient_channel: u32, - pending_eof: bool, - pending_close: bool, - ) -> ChannelParams { - ChannelParams { - recipient_channel, - sender_channel, - recipient_window_size: 1024, - sender_window_size: 1024, - recipient_maximum_packet_size: 1024, - sender_maximum_packet_size: 1024, - confirmed: true, - wants_reply: false, - pending_data: VecDeque::from([(Bytes::from_static(b"hello"), None, 0)]), - pending_eof, - pending_close, - } - } - - fn packet_types(buf: &[u8]) -> Vec { - let mut packet_types = Vec::new(); - let mut cursor = 0; - - while cursor < buf.len() { - let packet_len = BigEndian::read_u32(&buf[cursor..cursor + 4]) as usize; - packet_types.push(buf[cursor + 4]); - cursor += 4 + packet_len; - } - - packet_types - } - - fn clear_packet_types(buf: &[u8]) -> Vec { - let mut packet_types = Vec::new(); - let mut cursor = 0; - - while cursor < buf.len() { - let packet_len = BigEndian::read_u32(&buf[cursor..cursor + 4]) as usize; - packet_types.push(buf[cursor + 5]); - cursor += 4 + packet_len; - } - - packet_types - } - - fn test_ready_channel(sender_channel: ChannelId, recipient_channel: u32) -> ChannelParams { - let mut channel = test_channel(sender_channel, recipient_channel, false, false); - channel.pending_data.clear(); - channel - } - - fn test_channel_windowed( - sender_channel: ChannelId, - recipient_channel: u32, - window_size: u32, - pending_eof: bool, - pending_close: bool, - ) -> ChannelParams { - ChannelParams { - recipient_channel, - sender_channel, - recipient_window_size: window_size, - sender_window_size: 1024, - recipient_maximum_packet_size: 1024, - sender_maximum_packet_size: 1024, - confirmed: true, - wants_reply: false, - pending_data: VecDeque::from([(Bytes::from_static(b"hello"), None, 0)]), - pending_eof, - pending_close, - } - } - - // flush_pending (single-channel path) - - #[test] - fn flush_pending_replays_deferred_eof_once() { - let channel_id = ChannelId(10); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 42, true, false)); - - encrypted.flush_pending(channel_id).unwrap(); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF] - ); - assert!(!encrypted.channels[&channel_id].pending_eof); - - // Second flush must not re-emit EOF. - encrypted.flush_pending(channel_id).unwrap(); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF] - ); - } - - #[test] - fn flush_pending_replays_deferred_close_and_removes_channel() { - let channel_id = ChannelId(11); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 43, true, true)); - - encrypted.flush_pending(channel_id).unwrap(); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF, msg::CHANNEL_CLOSE] - ); - assert!(!encrypted.channels.contains_key(&channel_id)); - } - - #[test] - fn flush_pending_no_controls_when_incomplete() { - // Window smaller than data: flush is incomplete, EOF/CLOSE must not be sent. - let channel_id = ChannelId(12); - let mut encrypted = test_encrypted(); - encrypted.channels.insert( - channel_id, - test_channel_windowed(channel_id, 44, 3, true, true), - ); - - encrypted.flush_pending(channel_id).unwrap(); - // Only partial data fits; no EOF or CLOSE yet. - assert_eq!(packet_types(&encrypted.write), vec![msg::CHANNEL_DATA]); - assert!(encrypted.channels.contains_key(&channel_id)); - assert!(encrypted.channels[&channel_id].pending_eof); - assert!(encrypted.channels[&channel_id].pending_close); - } - - // flush_all_pending (multi-channel path) - - #[test] - fn flush_all_pending_replays_deferred_eof_once() { - let channel_id = ChannelId(1); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 42, true, false)); - - encrypted.flush_all_pending().unwrap(); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF] - ); - assert!(!encrypted.channels[&channel_id].pending_eof); - - encrypted.flush_all_pending().unwrap(); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF] - ); - } - - #[test] - fn flush_all_pending_replays_deferred_close_and_removes_channel() { - let channel_id = ChannelId(2); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 43, true, true)); - - encrypted.flush_all_pending().unwrap(); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF, msg::CHANNEL_CLOSE] - ); - assert!(!encrypted.channels.contains_key(&channel_id)); - } - - #[test] - fn flush_all_pending_replays_deferred_controls_across_channels() { - let channel_a = ChannelId(3); - let channel_b = ChannelId(4); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_a, test_channel(channel_a, 44, true, false)); - encrypted - .channels - .insert(channel_b, test_channel(channel_b, 45, false, true)); - - encrypted.flush_all_pending().unwrap(); - - let packet_types = packet_types(&encrypted.write); - assert_eq!( - packet_types - .iter() - .filter(|&&msg_type| msg_type == msg::CHANNEL_DATA) - .count(), - 2 - ); - assert_eq!( - packet_types - .iter() - .filter(|&&msg_type| msg_type == msg::CHANNEL_EOF) - .count(), - 1 - ); - assert_eq!( - packet_types - .iter() - .filter(|&&msg_type| msg_type == msg::CHANNEL_CLOSE) - .count(), - 1 - ); - assert!(encrypted.channels.contains_key(&channel_a)); - assert!(!encrypted.channels.contains_key(&channel_b)); - } - - #[test] - fn data_queues_behind_existing_pending_data_when_not_rekeying() { - let channel_id = ChannelId(5); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 42, false, false)); - - let channel = &encrypted.channels[&channel_id]; - let initial_pending = channel.pending_data.len(); - assert!(initial_pending > 0); - let initial_front = channel.pending_data.front().unwrap(); - let initial_front_data = initial_front.0.to_vec(); - let initial_front_ext = initial_front.1; - - encrypted - .data(channel_id, Bytes::from_static(b"new"), false) - .unwrap(); - - let channel = &encrypted.channels[&channel_id]; - assert_eq!(channel.pending_data.len(), initial_pending + 1); - assert_eq!( - channel.pending_data.front().unwrap().0.as_ref(), - initial_front_data.as_slice() - ); - assert_eq!(channel.pending_data.front().unwrap().1, initial_front_ext); - assert_eq!(channel.pending_data.back().unwrap().0.as_ref(), b"new"); - assert_eq!(channel.pending_data.back().unwrap().1, None); - assert!(encrypted.write.is_empty()); - } - - #[test] - fn extended_data_queues_behind_existing_pending_data_when_not_rekeying() { - let channel_id = ChannelId(6); - let ext = 1; - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 42, false, false)); - - let channel = &encrypted.channels[&channel_id]; - let initial_pending = channel.pending_data.len(); - assert!(initial_pending > 0); - let initial_front = channel.pending_data.front().unwrap(); - let initial_front_data = initial_front.0.to_vec(); - let initial_front_ext = initial_front.1; - - encrypted - .extended_data(channel_id, ext, Bytes::from_static(b"new"), false) - .unwrap(); - - let channel = &encrypted.channels[&channel_id]; - assert_eq!(channel.pending_data.len(), initial_pending + 1); - assert_eq!( - channel.pending_data.front().unwrap().0.as_ref(), - initial_front_data.as_slice() - ); - assert_eq!(channel.pending_data.front().unwrap().1, initial_front_ext); - assert_eq!(channel.pending_data.back().unwrap().0.as_ref(), b"new"); - assert_eq!(channel.pending_data.back().unwrap().1, Some(ext)); - assert!(encrypted.write.is_empty()); - } - - #[test] - fn flush_pending_with_writer_matches_staged_channel_data() { - let channel_id = ChannelId(7); - let mut staged = test_encrypted(); - let mut direct = test_encrypted(); - staged - .channels - .insert(channel_id, test_channel(channel_id, 42, false, false)); - direct - .channels - .insert(channel_id, test_channel(channel_id, 42, false, false)); - - let mut staged_writer = PacketWriter::clear(); - staged.flush_pending(channel_id).unwrap(); - staged - .flush(&Limits::default(), &mut staged_writer) - .unwrap(); - - let mut direct_writer = PacketWriter::clear(); - direct - .flush_pending_with_writer(&mut direct_writer, channel_id) - .unwrap(); - - assert_eq!(direct_writer.buffer().buffer, staged_writer.buffer().buffer); - assert_eq!( - direct.channels[&channel_id].recipient_window_size, - staged.channels[&channel_id].recipient_window_size - ); - assert!(direct.channels[&channel_id].pending_data.is_empty()); - } - - #[test] - fn flush_pending_with_writer_matches_staged_extended_data() { - let channel_id = ChannelId(8); - let mut staged = test_encrypted(); - let mut direct = test_encrypted(); - let mut staged_channel = test_channel(channel_id, 42, false, false); - staged_channel.pending_data = - VecDeque::from([(Bytes::from_static(b"hello"), Some(1), 0)]); - let mut direct_channel = test_channel(channel_id, 42, false, false); - direct_channel.pending_data = - VecDeque::from([(Bytes::from_static(b"hello"), Some(1), 0)]); - staged.channels.insert(channel_id, staged_channel); - direct.channels.insert(channel_id, direct_channel); - - let mut staged_writer = PacketWriter::clear(); - staged.flush_pending(channel_id).unwrap(); - staged - .flush(&Limits::default(), &mut staged_writer) - .unwrap(); - - let mut direct_writer = PacketWriter::clear(); - direct - .flush_pending_with_writer(&mut direct_writer, channel_id) - .unwrap(); - - assert_eq!(direct_writer.buffer().buffer, staged_writer.buffer().buffer); - assert!(direct.channels[&channel_id].pending_data.is_empty()); - } - - #[test] - fn flush_pending_with_writer_falls_back_when_write_queue_nonempty() { - let channel_id = ChannelId(9); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 42, false, false)); - push_packet!(encrypted.write, encrypted.write.push(msg::REQUEST_SUCCESS)); - - let mut writer = PacketWriter::clear(); - encrypted - .flush_pending_with_writer(&mut writer, channel_id) - .unwrap(); - - assert!(writer.buffer().buffer.is_empty()); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::REQUEST_SUCCESS, msg::CHANNEL_DATA] - ); - } - - #[test] - fn flush_pending_with_writer_preserves_partial_window_remainder() { - let channel_id = ChannelId(13); - let payload = Bytes::from_static(b"abcdef"); - let mut encrypted = test_encrypted(); - let mut channel = test_channel_windowed(channel_id, 42, 3, false, false); - channel.pending_data = VecDeque::from([(payload.clone(), None, 0)]); - encrypted.channels.insert(channel_id, channel); - - let mut writer = PacketWriter::clear(); - encrypted - .flush_pending_with_writer(&mut writer, channel_id) - .unwrap(); - - let channel = &encrypted.channels[&channel_id]; - assert_eq!( - clear_packet_types(&writer.buffer().buffer), - vec![msg::CHANNEL_DATA] - ); - assert_eq!(channel.recipient_window_size, 0); - assert_eq!(channel.pending_data.len(), 1); - let pending = channel.pending_data.back().unwrap(); - assert_eq!(pending.0, payload); - assert_eq!(pending.1, None); - assert_eq!(pending.2, 3); - } - - #[test] - fn flush_pending_with_writer_emits_controls_after_replayed_data() { - let channel_id = ChannelId(14); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 42, true, true)); - - let mut writer = PacketWriter::clear(); - encrypted - .flush_pending_with_writer(&mut writer, channel_id) - .unwrap(); - encrypted.flush(&Limits::default(), &mut writer).unwrap(); - - assert_eq!( - clear_packet_types(&writer.buffer().buffer), - vec![msg::CHANNEL_DATA, msg::CHANNEL_EOF, msg::CHANNEL_CLOSE] - ); - assert!(!encrypted.channels.contains_key(&channel_id)); - } - - #[test] - fn data_direct_matches_staged_channel_data() { - let channel_id = ChannelId(20); - let payload = Bytes::from_static(b"direct channel data"); - let mut staged = test_encrypted(); - let mut direct = test_encrypted(); - staged - .channels - .insert(channel_id, test_ready_channel(channel_id, 42)); - direct - .channels - .insert(channel_id, test_ready_channel(channel_id, 42)); - - let mut staged_writer = PacketWriter::clear(); - staged.data(channel_id, payload.clone(), false).unwrap(); - staged - .flush(&Limits::default(), &mut staged_writer) - .unwrap(); - - let mut direct_writer = PacketWriter::clear(); - direct - .data_with_writer(&mut direct_writer, channel_id, payload, false) - .unwrap(); - - assert_eq!( - direct_writer.buffer().buffer, - staged_writer.buffer().buffer - ); - assert_eq!( - direct.channels[&channel_id].recipient_window_size, - staged.channels[&channel_id].recipient_window_size - ); - assert!(direct.channels[&channel_id].pending_data.is_empty()); - } - - #[test] - fn extended_data_direct_matches_staged_channel_data() { - let channel_id = ChannelId(21); - let payload = Bytes::from_static(b"direct extended channel data"); - let mut staged = test_encrypted(); - let mut direct = test_encrypted(); - staged - .channels - .insert(channel_id, test_ready_channel(channel_id, 43)); - direct - .channels - .insert(channel_id, test_ready_channel(channel_id, 43)); - - let mut staged_writer = PacketWriter::clear(); - staged - .extended_data(channel_id, 1, payload.clone(), false) - .unwrap(); - staged - .flush(&Limits::default(), &mut staged_writer) - .unwrap(); - - let mut direct_writer = PacketWriter::clear(); - direct - .extended_data_with_writer(&mut direct_writer, channel_id, 1, payload, false) - .unwrap(); - - assert_eq!( - direct_writer.buffer().buffer, - staged_writer.buffer().buffer - ); - assert_eq!( - direct.channels[&channel_id].recipient_window_size, - staged.channels[&channel_id].recipient_window_size - ); - assert!(direct.channels[&channel_id].pending_data.is_empty()); - } - - #[test] - fn data_direct_is_disabled_when_write_queue_nonempty() { - let channel_id = ChannelId(22); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_ready_channel(channel_id, 44)); - push_packet!(encrypted.write, encrypted.write.push(msg::REQUEST_SUCCESS)); - - let mut writer = PacketWriter::clear(); - encrypted - .data_with_writer(&mut writer, channel_id, Bytes::from_static(b"new"), false) - .unwrap(); - - assert!(writer.buffer().buffer.is_empty()); - assert_eq!( - packet_types(&encrypted.write), - vec![msg::REQUEST_SUCCESS, msg::CHANNEL_DATA] - ); - - encrypted.flush(&Limits::default(), &mut writer).unwrap(); - assert_eq!( - clear_packet_types(&writer.buffer().buffer), - vec![msg::REQUEST_SUCCESS, msg::CHANNEL_DATA] - ); - } - - #[test] - fn data_staged_large_payload_when_write_queue_nonempty_preserves_order_and_chunks() { - let channel_id = ChannelId(26); - let mut encrypted = test_encrypted(); - let mut channel = test_ready_channel(channel_id, 48); - channel.recipient_window_size = 256 * 1024; - channel.recipient_maximum_packet_size = 32 * 1024; - encrypted.channels.insert(channel_id, channel); - push_packet!(encrypted.write, encrypted.write.push(msg::REQUEST_SUCCESS)); - - let mut writer = PacketWriter::clear(); - encrypted - .data_with_writer( - &mut writer, - channel_id, - Bytes::from(vec![0x5a; 256 * 1024]), - false, - ) - .unwrap(); - - assert!(writer.buffer().buffer.is_empty()); - let packet_types = packet_types(&encrypted.write); - assert_eq!(packet_types.first(), Some(&msg::REQUEST_SUCCESS)); - assert_eq!(packet_types.len(), 9); - assert!( - packet_types - .iter() - .skip(1) - .all(|&msg_type| msg_type == msg::CHANNEL_DATA) - ); - assert!(encrypted.channels[&channel_id].pending_data.is_empty()); - assert_eq!(encrypted.channels[&channel_id].recipient_window_size, 0); - } - - #[test] - fn data_staged_rejects_zero_recipient_max_packet_size() { - let channel_id = ChannelId(27); - let mut encrypted = test_encrypted(); - let mut channel = test_ready_channel(channel_id, 49); - channel.recipient_maximum_packet_size = 0; - encrypted.channels.insert(channel_id, channel); - - let result = encrypted.data(channel_id, Bytes::from_static(b"new"), false); - - assert!(matches!(result, Err(crate::Error::Inconsistent))); - assert!(encrypted.write.is_empty()); - assert_eq!(encrypted.channels[&channel_id].recipient_window_size, 1024); - } - - #[test] - fn data_direct_rejects_zero_recipient_max_packet_size() { - let channel_id = ChannelId(28); - let mut encrypted = test_encrypted(); - let mut channel = test_ready_channel(channel_id, 50); - channel.recipient_maximum_packet_size = 0; - encrypted.channels.insert(channel_id, channel); - - let mut writer = PacketWriter::clear(); - let result = - encrypted.data_with_writer(&mut writer, channel_id, Bytes::from_static(b"new"), false); - - assert!(matches!(result, Err(crate::Error::Inconsistent))); - assert!(writer.buffer().buffer.is_empty()); - assert!(encrypted.write.is_empty()); - assert_eq!(encrypted.channels[&channel_id].recipient_window_size, 1024); - } - - #[test] - fn data_direct_is_disabled_while_rekeying() { - let channel_id = ChannelId(23); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_ready_channel(channel_id, 45)); - - let mut writer = PacketWriter::clear(); - encrypted - .data_with_writer(&mut writer, channel_id, Bytes::from_static(b"new"), true) - .unwrap(); - - let channel = &encrypted.channels[&channel_id]; - assert_eq!(channel.pending_data.len(), 1); - assert_eq!(channel.pending_data.back().unwrap().0.as_ref(), b"new"); - assert_eq!(channel.pending_data.back().unwrap().1, None); - assert!(encrypted.write.is_empty()); - assert!(writer.buffer().buffer.is_empty()); - } - - #[test] - fn data_direct_queues_remainder_when_window_is_partial() { - let channel_id = ChannelId(24); - let payload = Bytes::from_static(b"abcdef"); - let mut encrypted = test_encrypted(); - let mut channel = test_ready_channel(channel_id, 46); - channel.recipient_window_size = 3; - encrypted.channels.insert(channel_id, channel); - - let mut writer = PacketWriter::clear(); - encrypted - .data_with_writer(&mut writer, channel_id, payload.clone(), false) - .unwrap(); - - let channel = &encrypted.channels[&channel_id]; - assert_eq!(clear_packet_types(&writer.buffer().buffer), vec![msg::CHANNEL_DATA]); - assert_eq!(channel.recipient_window_size, 0); - assert_eq!(channel.pending_data.len(), 1); - let pending = channel.pending_data.back().unwrap(); - assert_eq!(pending.0, payload); - assert_eq!(pending.1, None); - assert_eq!(pending.2, 3); - } - - #[test] - fn data_direct_disabled_behind_existing_pending_data() { - let channel_id = ChannelId(25); - let mut encrypted = test_encrypted(); - encrypted - .channels - .insert(channel_id, test_channel(channel_id, 47, false, false)); - - let mut writer = PacketWriter::clear(); - encrypted - .data_with_writer(&mut writer, channel_id, Bytes::from_static(b"new"), false) - .unwrap(); - - let channel = &encrypted.channels[&channel_id]; - assert_eq!(channel.pending_data.len(), 2); - assert_eq!(channel.pending_data.front().unwrap().0.as_ref(), b"hello"); - assert_eq!(channel.pending_data.back().unwrap().0.as_ref(), b"new"); - assert!(encrypted.write.is_empty()); - assert!(writer.buffer().buffer.is_empty()); - } -} diff --git a/crates/bssh-russh/src/ssh_read.rs b/crates/bssh-russh/src/ssh_read.rs deleted file mode 100644 index 89772d7a..00000000 --- a/crates/bssh-russh/src/ssh_read.rs +++ /dev/null @@ -1,363 +0,0 @@ -use std::pin::Pin; - -use futures::task::*; -use log::trace; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf}; - -use crate::Error; - -const SSH_ID_BUF_SIZE: usize = 256; -const SSH_ID_MAX_LINE_LEN: usize = 255; -const SSH_ID_MAX_PRE_BANNER_LINES: usize = 20; - -/// The buffer to read the identification string (first line in the -/// protocol). Not sensitive data — just protocol version exchange. -struct ReadSshIdBuffer { - pub buf: Box<[u8; SSH_ID_BUF_SIZE]>, - pub total: usize, - pub bytes_read: usize, - pub sshid_len: usize, - pub pre_banner_lines: usize, -} - -impl ReadSshIdBuffer { - pub fn id(&self) -> &[u8] { - #[allow(clippy::indexing_slicing)] // length checked - &self.buf[..self.sshid_len] - } - - pub fn new() -> ReadSshIdBuffer { - ReadSshIdBuffer { - buf: Box::new([0; SSH_ID_BUF_SIZE]), - sshid_len: 0, - bytes_read: 0, - total: 0, - pre_banner_lines: 0, - } - } - - fn line(&self) -> Option<(usize, usize)> { - if self.total < 2 { - return None; - } - #[allow(clippy::indexing_slicing)] // loop bounds keep i + 1 in range - for i in 0..self.total - 1 { - if self.buf[i] == b'\r' && self.buf[i + 1] == b'\n' { - return Some((i, i + 2)); - } - if self.buf[i + 1] == b'\n' { - return Some((i + 1, i + 2)); - } - } - None - } - - fn discard_line(&mut self) { - let remaining = self.total - self.bytes_read; - self.buf.copy_within(self.bytes_read..self.total, 0); - self.total = remaining; - self.bytes_read = 0; - } -} - -impl std::fmt::Debug for ReadSshIdBuffer { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "ReadSshId {:?}", self.id()) - } -} - -/// SshRead is the same as R, plus a small buffer in the beginning to -/// read the identification string. After the first line in the -/// connection, the `id` parameter is never used again. -pub struct SshRead { - id: Option, - pub r: R, -} - -impl SshRead { - pub fn split(self) -> (SshRead>, tokio::io::WriteHalf) { - let (r, w) = tokio::io::split(self.r); - (SshRead { id: self.id, r }, w) - } -} - -impl AsyncRead for SshRead { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context, - buf: &mut ReadBuf, - ) -> Poll> { - if let Some(mut id) = self.id.take() { - trace!("id {:?} {:?}", id.total, id.bytes_read); - if id.total > id.bytes_read { - let total = id.total.min(id.bytes_read + buf.remaining()); - #[allow(clippy::indexing_slicing)] // length checked - buf.put_slice(&id.buf[id.bytes_read..total]); - id.bytes_read += total - id.bytes_read; - self.id = Some(id); - return Poll::Ready(Ok(())); - } - } - AsyncRead::poll_read(Pin::new(&mut self.get_mut().r), cx, buf) - } -} - -impl std::io::Write for SshRead { - fn write(&mut self, buf: &[u8]) -> Result { - self.r.write(buf) - } - fn flush(&mut self) -> Result<(), std::io::Error> { - self.r.flush() - } -} - -impl AsyncWrite for SshRead { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - AsyncWrite::poll_write(Pin::new(&mut self.r), cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - AsyncWrite::poll_flush(Pin::new(&mut self.r), cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context, - ) -> Poll> { - AsyncWrite::poll_shutdown(Pin::new(&mut self.r), cx) - } -} - -impl SshRead { - pub fn new(r: R) -> Self { - SshRead { - id: Some(ReadSshIdBuffer::new()), - r, - } - } - - pub async fn read_ssh_id(&mut self) -> Result<&[u8], Error> { - self.read_ssh_id_inner(true).await - } - - pub async fn read_client_ssh_id(&mut self) -> Result<&[u8], Error> { - self.read_ssh_id_inner(false).await - } - - async fn read_ssh_id_inner(&mut self, allow_pre_banner_lines: bool) -> Result<&[u8], Error> { - let ssh_id = self.id.as_mut().ok_or(Error::Inconsistent)?; - loop { - let i = if let Some((line_len, bytes_read)) = ssh_id.line() { - ssh_id.bytes_read = bytes_read; - line_len - } else { - trace!("read_ssh_id: reading"); - - #[allow(clippy::indexing_slicing)] // length checked - let n = AsyncReadExt::read(&mut self.r, &mut ssh_id.buf[ssh_id.total..]).await?; - trace!("read {n:?}"); - - ssh_id.total += n; - #[allow(clippy::indexing_slicing)] // length checked - { - trace!("{:?}", std::str::from_utf8(&ssh_id.buf[..ssh_id.total])); - } - if n == 0 { - return Err(Error::Disconnect); - } - - if let Some((line_len, bytes_read)) = ssh_id.line() { - ssh_id.bytes_read = bytes_read; - line_len - } else if ssh_id.total >= SSH_ID_MAX_LINE_LEN { - return Err(Error::Version); - } else { - trace!("bytes_read: {:?}", ssh_id.bytes_read); - continue; - } - }; - - if ssh_id.bytes_read > 0 { - if ssh_id.bytes_read > SSH_ID_MAX_LINE_LEN { - return Err(Error::Version); - } - // If we have a full line, handle it. - if i >= 8 { - // Check if we have a valid SSH protocol identifier - #[allow(clippy::indexing_slicing)] - if let Ok(s) = std::str::from_utf8(&ssh_id.buf[..i]) { - if s.starts_with("SSH-1.99-") || s.starts_with("SSH-2.0-") { - ssh_id.sshid_len = i; - return Ok(ssh_id.id()); - } - } - } - if !allow_pre_banner_lines { - return Err(Error::Version); - } - ssh_id.pre_banner_lines += 1; - if ssh_id.pre_banner_lines > SSH_ID_MAX_PRE_BANNER_LINES { - return Err(Error::Version); - } - // Else, it is a "preliminary" (see - // https://tools.ietf.org/html/rfc4253#section-4.2), - // and we can discard it and read the next one. - ssh_id.discard_line(); - } - trace!("bytes_read: {:?}", ssh_id.bytes_read); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::iter; - - #[tokio::test] - async fn test_ssh_id_openssh() { - let data = "SSH-2.0-OpenSSH_10.2\r\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-OpenSSH_10.2"); - } - - #[tokio::test] - async fn test_ssh_id_openssh_7_4() { - let data = "SSH-2.0-OpenSSH_7.4\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-OpenSSH_7.4"); - } - - #[tokio::test] - async fn test_ssh_id_too_long() { - let data = String::from_iter(iter::once("SSH-2.0-").chain( - iter::repeat("A").take(500))); - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Version))); - } - - #[tokio::test] - async fn test_ssh_id_accepts_maximum_line_length() { - let data = format!("SSH-2.0-{}\r\n", "A".repeat(245)); - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received.len(), SSH_ID_MAX_LINE_LEN - 2); - } - - #[tokio::test] - async fn test_ssh_id_rejects_oversized_line_with_terminator() { - let data = format!("SSH-2.0-{}\r\n", "A".repeat(246)); - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Version))); - } - - #[tokio::test] - async fn test_ssh_id_rejects_too_many_pre_banner_lines() { - let data = format!( - "{}SSH-2.0-OpenSSH_10.2\r\n", - "debug\r\n".repeat(SSH_ID_MAX_PRE_BANNER_LINES + 1) - ); - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Version))); - } - - #[tokio::test] - async fn test_server_ssh_id_rejects_pre_banner_line() { - let data = "debug\r\nSSH-2.0-OpenSSH_10.2\r\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_client_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Version))); - } - - #[tokio::test] - async fn test_ssh_id_empty() { - let data = ""; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Disconnect))); - } - - #[tokio::test] - async fn test_ssh_id_almost_empty_cr_nl() { - let data = "SSH-2.0-\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-"); - } - - #[tokio::test] - async fn test_ssh_id_almost_empty_nl() { - let data = "SSH-2.0-\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-"); - } - - #[tokio::test] - async fn test_ssh_id_newline() { - let data = "\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Disconnect))); - } - - #[tokio::test] - async fn test_ssh_id_contains_cr() { - // A \r that isn't followed by \n has no special meaning - let data = "SSH-2.0-OpenSSH\r10.2\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-OpenSSH\r10.2"); - } - - #[tokio::test] - async fn test_ssh_id_trailing_cr() { - // Verify this doesn't cause an out-of-bounds access when testing for \r\n - let data = "SSH-2.0-OpenSSH_10.2\r"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await; - assert!(matches!(received.err(), Some(Error::Disconnect))); - } - - #[tokio::test] - async fn test_ssh_id_nl_cr() { - // Like \r\n but backwards - let data = "SSH-2.0-OpenSSH_10.2\n\r"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-OpenSSH_10.2"); - } - - #[tokio::test] - async fn test_ssh_id_nl_cr_nl() { - // Like \r\n but backwards, but also part of \r\n - let data = "SSH-2.0-OpenSSH_10.2\n\r\n"; - let mut read = SshRead::new(data.as_bytes()); - - let received = read.read_ssh_id().await.unwrap(); - assert_eq!(received, b"SSH-2.0-OpenSSH_10.2"); - } -} diff --git a/crates/bssh-russh/src/sshbuffer.rs b/crates/bssh-russh/src/sshbuffer.rs deleted file mode 100644 index 234693ab..00000000 --- a/crates/bssh-russh/src/sshbuffer.rs +++ /dev/null @@ -1,574 +0,0 @@ -// Copyright 2016 Pierre-Étienne Meunier -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use core::fmt; -use std::borrow::Cow; -use std::num::Wrapping; - -use bytes::{Bytes, BytesMut}; -use log::debug; -use ssh_encoding::Writer; -use super::cipher::SealingKey; -use compression::Compress; -use tokio::io::{AsyncWrite, AsyncWriteExt}; - -use super::*; - -/// The SSH client/server identification string. -#[derive(Debug)] -pub enum SshId { - /// When sending the id, append RFC standard `\r\n`. Example: `SshId::Standard("SSH-2.0-acme")` - Standard(Cow<'static, str>), - /// When sending the id, use this buffer as it is and do not append additional line terminators. - Raw(Cow<'static, str>), -} - -impl SshId { - pub(crate) fn as_kex_hash_bytes(&self) -> &[u8] { - match self { - Self::Standard(s) => s.as_bytes(), - Self::Raw(s) => s.trim_end_matches(['\n', '\r']).as_bytes(), - } - } - - /// Write the SSH identification string to a buffer. - /// Buffer is not sensitive - SSH identification strings are public protocol data. - pub(crate) fn write(&self, buffer: &mut Vec) { - match self { - Self::Standard(s) => buffer.extend_from_slice(format!("{s}\r\n").as_bytes()), - Self::Raw(s) => buffer.extend_from_slice(s.as_bytes()), - } - } -} - -#[test] -fn test_ssh_id() { - let mut buffer = Vec::new(); - SshId::Standard("SSH-2.0-acme".into()).write(&mut buffer); - assert_eq!(&buffer[..], b"SSH-2.0-acme\r\n"); - - let mut buffer = Vec::new(); - SshId::Raw("SSH-2.0-raw\n".into()).write(&mut buffer); - assert_eq!(&buffer[..], b"SSH-2.0-raw\n"); - - assert_eq!( - SshId::Standard("SSH-2.0-acme".into()).as_kex_hash_bytes(), - b"SSH-2.0-acme" - ); - assert_eq!( - SshId::Raw("SSH-2.0-raw\n".into()).as_kex_hash_bytes(), - b"SSH-2.0-raw" - ); -} - -#[test] -fn test_write_packet_leaves_reusable_buffer_for_cold_path_packets() { - let mut writer = PacketWriter::clear(); - let large_len = 128 * 1024; - let packet_buffer_capacity = writer.packet_buffer.capacity(); - - writer - .write_packet(|buf| { - buf.resize(buf.len() + large_len, 0x5a); - Ok(()) - }) - .unwrap(); - assert_eq!(writer.packet_buffer.capacity(), packet_buffer_capacity); -} - -#[test] -fn reserve_cleartext_packet_output_reserves_output_capacity() { - let mut writer = PacketWriter::clear(); - let payload_bytes = 4096; - let packet_count = 4; - - writer.reserve_cleartext_packet_output(payload_bytes, packet_count); - - let expected = payload_bytes - + packet_count * (PacketWriter::PACKET_PREFIX_LEN + writer.cipher.tag_len() + 32); - assert!(writer.write_buffer.buffer.capacity() >= expected); - assert!(writer.write_buffer.buffer.is_empty()); -} - -#[cfg(feature = "flate2")] -#[test] -fn reserve_cleartext_packet_output_ignores_compressed_writer() { - let mut writer = PacketWriter::new(Box::new(cipher::clear::Key {}), zlib_compress()); - let capacity = writer.write_buffer.buffer.capacity(); - - writer.reserve_cleartext_packet_output(4096, 4); - - assert_eq!(writer.write_buffer.buffer.capacity(), capacity); -} - -#[test] -fn test_packet_returns_retained_bytes() { - let mut writer = PacketWriter::clear(); - let retained = writer - .packet(|buf| { - buf.extend_from_slice(b"abc"); - Ok(()) - }) - .unwrap(); - - assert_eq!(&retained[..], b"abc"); -} - -#[test] -fn packet_bytes_returns_retained_bytes() { - let mut writer = PacketWriter::clear(); - let retained = writer - .packet_bytes(|buf| { - buf.extend_from_slice(b"abc"); - Ok(()) - }) - .unwrap(); - - assert_eq!(&retained[..], b"abc"); -} - -#[test] -fn packet_bytes_matches_packet_output() { - let payload = b"abcdefghijklmno".to_vec(); - - let mut packet_writer = PacketWriter::clear(); - let packet_retained = packet_writer - .packet(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - let packet_buffer = packet_writer.buffer().buffer.clone(); - let packet_bytes = packet_writer.buffer().bytes; - - let mut packet_bytes_writer = PacketWriter::clear(); - let bytes_retained = packet_bytes_writer - .packet_bytes(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - - assert_eq!(packet_retained, bytes_retained); - assert_eq!(packet_bytes_writer.buffer().buffer, packet_buffer); - assert_eq!(packet_bytes_writer.buffer().bytes, packet_bytes); -} - -#[test] -fn test_write_packet_matches_clear_cipher_write_output() { - let payload = b"abcdefghijklmno".to_vec(); - - let mut expected = SSHBuffer::new(); - let mut clear = cipher::clear::Key {}; - clear.write(&payload, &mut expected); - - let mut writer = PacketWriter::clear(); - writer - .write_packet(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - - assert_eq!(writer.buffer().buffer, expected.buffer); - assert_eq!(writer.buffer().bytes, payload.len()); - assert_eq!(writer.buffer().seqn, Wrapping(1)); -} - -#[test] -fn test_write_packet_restores_output_buffer_on_error() { - let mut writer = PacketWriter::clear(); - writer - .write_packet(|buf| { - buf.extend_from_slice(b"ok"); - Ok(()) - }) - .unwrap(); - let before = writer.buffer().buffer.clone(); - - let err = writer.write_packet(|buf| { - buf.extend_from_slice(b"partial"); - Err(Error::Inconsistent) - }); - - assert!(matches!(err, Err(Error::Inconsistent))); - assert_eq!(writer.buffer().buffer, before); -} - -#[cfg(all(test, feature = "flate2"))] -fn zlib_compress() -> Compress { - let mut compress = Compress::None; - compression::Compression::Zlib.init_compress(&mut compress); - compress -} - -#[cfg(feature = "flate2")] -#[test] -fn test_write_packet_compressed_matches_clear_cipher_output() { - let payload = b"abcdefghijklmnoabcdefghijklmno".to_vec(); - - let mut expected = SSHBuffer::new(); - let mut clear = cipher::clear::Key {}; - let mut compress = zlib_compress(); - let mut compressed = Vec::new(); - let packet = compress.compress(&payload, &mut compressed).unwrap(); - clear.write(packet, &mut expected); - - let mut writer = PacketWriter::new(Box::new(cipher::clear::Key {}), zlib_compress()); - writer - .write_packet(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - - assert_eq!(writer.buffer().buffer, expected.buffer); - assert_eq!(writer.buffer().bytes, packet.len()); - assert_eq!(writer.buffer().seqn, Wrapping(1)); -} - -#[cfg(feature = "flate2")] -#[test] -fn test_packet_retains_plaintext_for_compressed_packets() { - let payload = b"abcdefghijklmnoabcdefghijklmno".to_vec(); - - let mut writer = PacketWriter::new(Box::new(cipher::clear::Key {}), zlib_compress()); - let retained = writer - .packet(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - - assert_eq!(&retained[..], &payload); -} - -#[cfg(feature = "flate2")] -#[test] -fn packet_bytes_compressed_matches_packet_output() { - let payload = b"abcdefghijklmnoabcdefghijklmno".to_vec(); - - let mut packet_writer = PacketWriter::new(Box::new(cipher::clear::Key {}), zlib_compress()); - let packet_retained = packet_writer - .packet(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - let packet_buffer = packet_writer.buffer().buffer.clone(); - let packet_bytes = packet_writer.buffer().bytes; - - let mut packet_bytes_writer = - PacketWriter::new(Box::new(cipher::clear::Key {}), zlib_compress()); - let bytes_retained = packet_bytes_writer - .packet_bytes(|buf| { - buf.extend_from_slice(&payload); - Ok(()) - }) - .unwrap(); - - assert_eq!(packet_retained, bytes_retained); - assert_eq!(packet_bytes_writer.buffer().buffer, packet_buffer); - assert_eq!(packet_bytes_writer.buffer().bytes, packet_bytes); -} - -/// SSH packet read/write buffer. Uses Vec (not CryptoVec/mlocked) because -/// packet data is not secret material. -#[derive(Debug, Default)] -pub struct SSHBuffer { - pub buffer: Vec, - pub len: usize, // next packet length. - pub bytes: usize, // total bytes written since the last rekey - // Sequence numbers are on 32 bits and wrap. - // https://tools.ietf.org/html/rfc4253#section-6.4 - pub seqn: Wrapping, -} - -impl SSHBuffer { - pub fn new() -> Self { - SSHBuffer { - buffer: Vec::new(), - len: 0, - bytes: 0, - seqn: Wrapping(0), - } - } - - pub fn send_ssh_id(&mut self, id: &SshId) { - id.write(&mut self.buffer); - } -} - -pub(crate) struct PacketBytesWriter { - buffer: BytesMut, -} - -impl Writer for PacketBytesWriter { - fn write(&mut self, bytes: &[u8]) -> ssh_encoding::Result<()> { - self.buffer.extend_from_slice(bytes); - Ok(()) - } -} - -impl PacketBytesWriter { - #[allow(dead_code)] - pub(crate) fn push(&mut self, byte: u8) { - self.buffer.extend_from_slice(&[byte]); - } - - #[allow(dead_code)] - pub(crate) fn extend_from_slice(&mut self, bytes: &[u8]) { - self.buffer.extend_from_slice(bytes); - } - - #[allow(dead_code)] - pub(crate) fn len(&self) -> usize { - self.buffer.len() - } - - #[allow(dead_code)] - pub(crate) fn is_empty(&self) -> bool { - self.buffer.is_empty() - } - - fn freeze(self) -> Bytes { - self.buffer.freeze() - } -} - -/// Incoming SSH packet after decryption and optional decompression. -/// Uses Vec (not CryptoVec/mlocked) because incoming network data is not secret. -#[derive(Debug)] -pub(crate) struct IncomingSshPacket { - pub buffer: Vec, - pub seqn: Wrapping, -} - -/// Packet writer for constructing and encrypting outgoing SSH packets. -pub(crate) struct PacketWriter { - cipher: Box, - compress: Compress, - packet_buffer: Vec, - write_buffer: SSHBuffer, -} - -impl Debug for PacketWriter { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("PacketWriter").finish() - } -} - -impl PacketWriter { - // SSH packet prefix = packet_length (cipher::PACKET_LENGTH_LEN bytes) - // + padding_length (1 byte). - const PACKET_PREFIX_LEN: usize = cipher::PACKET_LENGTH_LEN + 1; - - pub fn clear() -> Self { - Self::new(Box::new(cipher::clear::Key {}), Compress::None) - } - - pub fn new(cipher: Box, compress: Compress) -> Self { - Self { - cipher, - compress, - packet_buffer: Vec::new(), - write_buffer: SSHBuffer::new(), - } - } - - fn prepare_packet) -> Result<(), Error>>( - &mut self, - f: F, - ) -> Result, Error> { - let mut buf = std::mem::take(&mut self.packet_buffer); - buf.clear(); - match f(&mut buf) { - Ok(()) => Ok(buf), - Err(err) => { - self.packet_buffer = buf; - Err(err) - } - } - } - - fn write_packet_in_place) -> Result<(), Error>>( - &mut self, - f: F, - ) -> Result<(), Error> { - self.write_payload_into_output(|buffer, payload_start| { - f(buffer)?; - Ok(buffer.len() - payload_start) - }) - } - - fn write_payload_into_output(&mut self, f: F) -> Result<(), Error> - where - F: FnOnce(&mut Vec, usize) -> Result, - { - let offset = self.write_buffer.buffer.len(); - let payload_start = offset + Self::PACKET_PREFIX_LEN; - - self.write_buffer.buffer.resize(payload_start, 0); - match f(&mut self.write_buffer.buffer, payload_start) { - Ok(payload_len) => { - if payload_len == 0 { - self.write_buffer.buffer.truncate(offset); - return Ok(()); - } - - if let Some(message_type) = self.write_buffer.buffer.get(payload_start) { - debug!("> msg type {message_type:?}, len {payload_len}"); - } - - self.cipher - .finish_packet(offset, payload_len, &mut self.write_buffer); - Ok(()) - } - Err(err) => { - self.write_buffer.buffer.truncate(offset); - Err(err) - } - } - } - - fn write_compressed_payload_into_output(&mut self, buf: &[u8]) -> Result<(), Error> { - let offset = self.write_buffer.buffer.len(); - let payload_start = offset + Self::PACKET_PREFIX_LEN; - - self.write_buffer.buffer.resize(payload_start, 0); - match self - .compress - .compress_into(buf, &mut self.write_buffer.buffer, payload_start) - { - Ok(payload_len) => { - if payload_len == 0 { - self.write_buffer.buffer.truncate(offset); - return Ok(()); - } - - self.cipher - .finish_packet(offset, payload_len, &mut self.write_buffer); - Ok(()) - } - Err(err) => { - self.write_buffer.buffer.truncate(offset); - Err(err) - } - } - } - - pub fn packet_raw(&mut self, buf: &[u8]) -> Result<(), Error> { - if let Some(message_type) = buf.first() { - debug!("> msg type {message_type:?}, len {}", buf.len()); - if matches!(&self.compress, Compress::None) { - self.cipher.write(buf, &mut self.write_buffer); - } else { - self.write_compressed_payload_into_output(buf)?; - } - } - Ok(()) - } - - /// Sends a packet using the reusable plaintext packet buffer. - /// - /// The closure must append only the packet payload bytes. It must not - /// modify or truncate any existing contents in the provided buffer. - /// When compression is disabled, the buffer may already contain queued - /// packets and the reserved 5-byte packet header prefix for the packet - /// being built, so callers must only write new payload bytes starting at - /// the current end of the buffer. - pub fn write_packet) -> Result<(), Error>>( - &mut self, - f: F, - ) -> Result<(), Error> { - if matches!(&self.compress, Compress::None) { - return self.write_packet_in_place(f); - } - let buf = self.prepare_packet(f)?; - let result = self.packet_raw(&buf); - self.packet_buffer = buf; - result - } - - pub(crate) fn reserve_cleartext_packet_output( - &mut self, - payload_bytes: usize, - packet_count: usize, - ) { - if !matches!(&self.compress, Compress::None) { - return; - } - - // Padding is cipher-dependent and rounded to the cipher block size. - // Reserving a small fixed margin avoids repeated output-buffer growth - // without coupling callers to individual cipher padding formulas. - let per_packet_margin = Self::PACKET_PREFIX_LEN + self.cipher.tag_len() + 32; - let additional = payload_bytes.saturating_add(packet_count.saturating_mul(per_packet_margin)); - self.write_buffer.buffer.reserve(additional); - } - - /// Sends and returns the packet contents for callers that need to retain - /// the plaintext packet after it has been queued for encryption. - #[allow(dead_code)] - pub fn packet) -> Result<(), Error>>( - &mut self, - f: F, - ) -> Result { - let buf = self.prepare_packet(f)?; - if let Err(err) = self.packet_raw(&buf) { - self.packet_buffer = buf; - return Err(err); - } - Ok(Bytes::from(buf)) - } - - pub(crate) fn packet_bytes(&mut self, f: F) -> Result - where - F: FnOnce(&mut PacketBytesWriter) -> Result<(), Error>, - { - let mut buf = PacketBytesWriter { - buffer: BytesMut::new(), - }; - f(&mut buf)?; - let packet = buf.freeze(); - self.packet_raw(packet.as_ref())?; - Ok(packet) - } - - pub fn buffer(&mut self) -> &mut SSHBuffer { - &mut self.write_buffer - } - - pub fn compress(&mut self) -> &mut Compress { - &mut self.compress - } - - pub fn set_cipher(&mut self, cipher: Box) { - self.cipher = cipher; - } - - pub fn reset_seqn(&mut self) { - self.write_buffer.seqn = Wrapping(0); - } - - pub async fn flush_into(&mut self, w: &mut W) -> std::io::Result<()> { - if !self.write_buffer.buffer.is_empty() { - w.write_all(&self.write_buffer.buffer).await?; - w.flush().await?; - self.write_buffer.buffer.clear(); - } - Ok(()) - } -} diff --git a/crates/bssh-russh/src/tests.rs b/crates/bssh-russh/src/tests.rs deleted file mode 100644 index 7d84c754..00000000 --- a/crates/bssh-russh/src/tests.rs +++ /dev/null @@ -1,1408 +0,0 @@ -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // Allow unwraps, expects and panics in the test suite - -use futures::Future; - -use super::*; - -#[cfg(feature = "flate2")] -mod compress { - use std::borrow::Cow; - use std::collections::HashMap; - use std::io::Write; - use std::sync::{Arc, Mutex}; - - use keys::PrivateKeyWithHashAlg; - use log::debug; - use ssh_key::PrivateKey; - - use super::server::{Server as _, Session}; - use super::*; - use crate::cipher::MAXIMUM_DECOMPRESSED_PACKET_LEN; - use crate::server::Msg; - - const OVERSIZED_DEBUG_MESSAGE_LEN: usize = MAXIMUM_DECOMPRESSED_PACKET_LEN + 1024; - - #[tokio::test] - async fn compress_local_test() { - let _ = env_logger::try_init(); - - let client_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let mut config = server::Config::default(); - config.preferred = preferred_zlib(); - config.inactivity_timeout = None; // Some(std::time::Duration::from_secs(3)); - config.auth_rejection_time = std::time::Duration::from_secs(3); - config - .keys - .push(PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap()); - let config = Arc::new(config); - let mut sh = Server { - clients: Arc::new(Mutex::new(HashMap::new())), - id: 0, - }; - - let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = socket.local_addr().unwrap(); - - tokio::spawn(async move { - let (socket, _) = socket.accept().await.unwrap(); - let server = sh.new_client(socket.peer_addr().ok()); - server::run_stream(config, socket, server).await.unwrap(); - }); - - let mut config = client::Config::default(); - config.preferred = preferred_zlib(); - let config = Arc::new(config); - - let mut session = client::connect(config, addr, Client {}).await.unwrap(); - let authenticated = session - .authenticate_publickey( - std::env::var("USER").unwrap_or("user".to_owned()), - PrivateKeyWithHashAlg::new( - Arc::new(client_key), - session.best_supported_rsa_hash().await.unwrap().flatten(), - ), - ) - .await - .unwrap() - .success(); - assert!(authenticated); - let mut channel = session.channel_open_session().await.unwrap(); - - let data = &b"Hello, world!"[..]; - channel.data(data).await.unwrap(); - let msg = channel.wait().await.unwrap(); - match msg { - ChannelMsg::Data { data: msg_data } => { - assert_eq!(*data, *msg_data) - } - msg => panic!("Unexpected message {msg:?}"), - } - } - - #[test] - fn oversized_debug_payload_can_stay_below_wire_cap() { - let payload = vec![b'A'; OVERSIZED_DEBUG_MESSAGE_LEN]; - let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best()); - encoder.write_all(&payload).unwrap(); - let compressed = encoder.finish().unwrap(); - - assert!( - compressed.len() < 256 * 1024, - "attacker-crafted compressed payload should stay below the normal SSH wire cap" - ); - } - - #[derive(Clone)] - struct Server { - clients: Arc>>, - id: usize, - } - - impl server::Server for Server { - type Handler = Self; - fn new_client(&mut self, _: Option) -> Self { - let s = self.clone(); - self.id += 1; - s - } - } - - impl server::Handler for Server { - type Error = super::Error; - - async fn channel_open_session( - &mut self, - channel: Channel, - session: &mut Session, - ) -> Result { - { - let mut clients = self.clients.lock().unwrap(); - clients.insert((self.id, channel.id()), session.handle()); - } - Ok(true) - } - async fn auth_publickey( - &mut self, - _: &str, - _: &crate::keys::ssh_key::PublicKey, - ) -> Result { - debug!("auth_publickey"); - Ok(server::Auth::Accept) - } - async fn data( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> Result<(), Self::Error> { - debug!("server data = {:?}", std::str::from_utf8(data)); - session.data(channel, data.to_vec())?; - Ok(()) - } - } - - struct Client {} - - impl client::Handler for Client { - type Error = super::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &crate::keys::ssh_key::PublicKey, - ) -> Result { - // println!("check_server_key: {:?}", server_public_key); - Ok(true) - } - } - - fn preferred_zlib() -> Preferred { - Preferred { - compression: Cow::Borrowed(&[ - crate::compression::ZLIB, - crate::compression::ZLIB_LEGACY, - crate::compression::NONE, - ]), - ..Preferred::DEFAULT - } - } -} - -mod channels { - use keys::PrivateKeyWithHashAlg; - use server::Session; - use ssh_key::PrivateKey; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - use super::*; - - async fn test_session( - client_handler: CH, - server_handler: SH, - run_client: RC, - run_server: RS, - ) where - RC: FnOnce(crate::client::Handle) -> F1 + Send + Sync + 'static, - RS: FnOnce(crate::server::Handle) -> F2 + Send + Sync + 'static, - F1: Future> + Send + Sync + 'static, - F2: Future + Send + Sync + 'static, - CH: crate::client::Handler + Send + Sync + 'static, - SH: crate::server::Handler + Send + Sync + 'static, - { - use std::sync::Arc; - - use crate::*; - - let _ = env_logger::try_init(); - - let client_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let mut config = server::Config::default(); - config.inactivity_timeout = None; - config.auth_rejection_time = std::time::Duration::from_secs(3); - config - .keys - .push(PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap()); - let config = Arc::new(config); - let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = socket.local_addr().unwrap(); - - let server_join = tokio::spawn(async move { - let (socket, _) = socket.accept().await.unwrap(); - - server::run_stream(config, socket, server_handler) - .await - .map_err(|_| ()) - .unwrap() - }); - - let client_join = tokio::spawn(async move { - let config = Arc::new(client::Config::default()); - let mut session = client::connect(config, addr, client_handler) - .await - .map_err(|_| ()) - .unwrap(); - let authenticated = session - .authenticate_publickey( - std::env::var("USER").unwrap_or("user".to_owned()), - PrivateKeyWithHashAlg::new(Arc::new(client_key), None), - ) - .await - .unwrap(); - assert!(authenticated.success()); - session - }); - - let (server_session, client_session) = tokio::join!(server_join, client_join); - let client_handle = tokio::spawn(run_client(client_session.unwrap())); - let server_handle = tokio::spawn(run_server(server_session.unwrap().handle())); - - let (server_session, client_session) = tokio::join!(server_handle, client_handle); - assert!(server_session.is_ok()); - assert!(client_session.is_ok()); - drop(client_session); - drop(server_session); - } - - #[tokio::test] - async fn test_server_channels() { - #[derive(Debug)] - struct Client {} - - impl client::Handler for Client { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - - async fn data( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut client::Session, - ) -> Result<(), Self::Error> { - assert_eq!(data, &b"hello world!"[..]); - session.data(channel, b"hey there!".to_vec())?; - Ok(()) - } - } - - struct ServerHandle { - did_auth: Option>, - } - - impl ServerHandle { - fn get_auth_waiter(&mut self) -> tokio::sync::oneshot::Receiver<()> { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.did_auth = Some(tx); - rx - } - } - - impl server::Handler for ServerHandle { - type Error = crate::Error; - - async fn auth_publickey( - &mut self, - _: &str, - _: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(server::Auth::Accept) - } - async fn auth_succeeded(&mut self, _session: &mut Session) -> Result<(), Self::Error> { - if let Some(a) = self.did_auth.take() { - a.send(()).unwrap(); - } - Ok(()) - } - } - - let mut sh = ServerHandle { did_auth: None }; - let a = sh.get_auth_waiter(); - test_session( - Client {}, - sh, - |c| async move { c }, - |s| async move { - a.await.unwrap(); - let mut ch = s.channel_open_session().await.unwrap(); - ch.data(&b"hello world!"[..]).await.unwrap(); - - let msg = ch.wait().await.unwrap(); - if let ChannelMsg::Data { data } = msg { - assert_eq!(&data[..], &b"hey there!"[..]); - } else { - panic!("Unexpected message {msg:?}"); - } - s - }, - ) - .await; - } - - #[tokio::test] - async fn test_channel_streams() { - #[derive(Debug)] - struct Client {} - - impl client::Handler for Client { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - } - - struct ServerHandle { - channel: Option>>, - } - - impl ServerHandle { - fn get_channel_waiter( - &mut self, - ) -> tokio::sync::oneshot::Receiver> { - let (tx, rx) = tokio::sync::oneshot::channel::>(); - self.channel = Some(tx); - rx - } - } - - impl server::Handler for ServerHandle { - type Error = crate::Error; - - async fn auth_publickey( - &mut self, - _: &str, - _: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(server::Auth::Accept) - } - - async fn channel_open_session( - &mut self, - channel: Channel, - _session: &mut server::Session, - ) -> Result { - if let Some(a) = self.channel.take() { - println!("channel open session {a:?}"); - a.send(channel).unwrap(); - } - Ok(true) - } - } - - let mut sh = ServerHandle { channel: None }; - let scw = sh.get_channel_waiter(); - - test_session( - Client {}, - sh, - |client| async move { - let ch = client.channel_open_session().await.unwrap(); - let mut stream = ch.into_stream(); - stream.write_all(&b"request"[..]).await.unwrap(); - - let mut buf = Vec::new(); - stream.read_buf(&mut buf).await.unwrap(); - assert_eq!(&buf, &b"response"[..]); - - stream.write_all(&b"reply"[..]).await.unwrap(); - - client - }, - |server| async move { - let channel = scw.await.unwrap(); - let mut stream = channel.into_stream(); - - let mut buf = Vec::new(); - stream.read_buf(&mut buf).await.unwrap(); - assert_eq!(&buf, &b"request"[..]); - - stream.write_all(&b"response"[..]).await.unwrap(); - - buf.clear(); - - stream.read_buf(&mut buf).await.unwrap(); - assert_eq!(&buf, &b"reply"[..]); - - server - }, - ) - .await; - } - - #[tokio::test] - async fn test_channel_objects() { - #[derive(Debug)] - struct Client {} - - impl client::Handler for Client { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - } - - struct ServerHandle {} - - impl ServerHandle {} - - impl server::Handler for ServerHandle { - type Error = crate::Error; - - async fn auth_publickey( - &mut self, - _: &str, - _: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(server::Auth::Accept) - } - - async fn channel_open_session( - &mut self, - mut channel: Channel, - _session: &mut Session, - ) -> Result { - tokio::spawn(async move { - while let Some(msg) = channel.wait().await { - match msg { - ChannelMsg::Data { data } => { - channel.data(&data[..]).await.unwrap(); - channel.close().await.unwrap(); - break; - } - _ => {} - } - } - }); - Ok(true) - } - } - - let sh = ServerHandle {}; - test_session( - Client {}, - sh, - |c| async move { - let mut ch = c.channel_open_session().await.unwrap(); - ch.data(&b"hello world!"[..]).await.unwrap(); - - let msg = ch.wait().await.unwrap(); - if let ChannelMsg::Data { data } = msg { - assert_eq!(&data[..], &b"hello world!"[..]); - } else { - panic!("Unexpected message {msg:?}"); - } - - // After the server closes the channel, we should receive an - // explicit Close message before the channel stream ends. - let msg = ch.wait().await.unwrap(); - assert!( - matches!(msg, ChannelMsg::Close), - "expected Close, got {msg:?}" - ); - assert!(ch.wait().await.is_none()); - c - }, - |s| async move { s }, - ) - .await; - } - - /// Verify that the server-side CHANNEL_CLOSE handler delivers - /// `ChannelMsg::Close` before the channel stream ends. - #[tokio::test] - async fn test_server_receives_close_on_client_close() { - #[derive(Debug)] - struct Client {} - - impl client::Handler for Client { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - } - - struct ServerHandle { - channel: Option>>, - } - - impl server::Handler for ServerHandle { - type Error = crate::Error; - - async fn auth_publickey( - &mut self, - _: &str, - _: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(server::Auth::Accept) - } - - async fn channel_open_session( - &mut self, - channel: Channel, - _session: &mut server::Session, - ) -> Result { - if let Some(tx) = self.channel.take() { - tx.send(channel).unwrap(); - } - Ok(true) - } - } - - let (tx, rx) = tokio::sync::oneshot::channel::>(); - let sh = ServerHandle { channel: Some(tx) }; - - test_session( - Client {}, - sh, - |c| async move { - let ch = c.channel_open_session().await.unwrap(); - ch.close().await.unwrap(); - c - }, - |s| async move { - let mut ch = rx.await.unwrap(); - // The server should receive an explicit Close message - // when the client closes the channel. - let msg = ch.wait().await.unwrap(); - assert!( - matches!(msg, ChannelMsg::Close), - "expected Close, got {msg:?}" - ); - assert!(ch.wait().await.is_none()); - s - }, - ) - .await; - } - - #[tokio::test] - async fn test_channel_window_size() { - #[derive(Debug)] - struct Client {} - - impl client::Handler for Client { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - } - - struct ServerHandle { - channel: Option>>, - } - - impl ServerHandle { - fn get_channel_waiter( - &mut self, - ) -> tokio::sync::oneshot::Receiver> { - let (tx, rx) = tokio::sync::oneshot::channel::>(); - self.channel = Some(tx); - rx - } - } - - impl server::Handler for ServerHandle { - type Error = crate::Error; - - async fn auth_publickey( - &mut self, - _: &str, - _: &crate::keys::ssh_key::PublicKey, - ) -> Result { - Ok(server::Auth::Accept) - } - - async fn channel_open_session( - &mut self, - channel: Channel, - _session: &mut server::Session, - ) -> Result { - if let Some(a) = self.channel.take() { - println!("channel open session {a:?}"); - a.send(channel).unwrap(); - } - Ok(true) - } - } - - let mut sh = ServerHandle { channel: None }; - let scw = sh.get_channel_waiter(); - - test_session( - Client {}, - sh, - |client| async move { - let ch = client.channel_open_session().await.unwrap(); - - let mut writer_1 = ch.make_writer(); - let jh_1 = tokio::spawn(async move { - let buf = [1u8; 1024 * 64]; - assert!(writer_1.write_all(&buf).await.is_ok()); - }); - let mut writer_2 = ch.make_writer(); - let jh_2 = tokio::spawn(async move { - let buf = [2u8; 1024 * 64]; - assert!(writer_2.write_all(&buf).await.is_ok()); - }); - - assert!(tokio::try_join!(jh_1, jh_2).is_ok()); - - client - }, - |server| async move { - let mut channel = scw.await.unwrap(); - - let mut total_data = 2 * 1024 * 64; - while let Some(msg) = channel.wait().await { - match msg { - ChannelMsg::Data { data } => { - total_data -= data.len(); - if total_data == 0 { - break; - } - } - _ => panic!("Unexpected message {msg:?}"), - } - } - - server - }, - ) - .await; - } -} - -mod gex { - use super::*; - - #[test] - fn peer_request_accepts_rfc4419_minimum_when_server_can_choose_stronger_group() { - let params = client::GexParams::from_peer_request(1024, 4097, 8192).unwrap(); - - assert_eq!(params.min_group_size(), 1024); - assert_eq!(params.preferred_group_size(), 4097); - assert_eq!(params.max_group_size(), 8192); - } - - #[test] - fn local_client_config_still_rejects_minimum_below_2048() { - let error = client::GexParams::for_client_config(1024, 4097, 8192).unwrap_err(); - - assert!(matches!(error, Error::InvalidConfig(_))); - } -} - -mod server_kex_junk { - use std::sync::Arc; - - use tokio::io::AsyncWriteExt; - - use super::server::Server as _; - use super::*; - - #[tokio::test] - async fn server_kex_junk_test() { - let _ = env_logger::try_init(); - - let config = server::Config::default(); - let config = Arc::new(config); - let mut sh = Server {}; - - let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = socket.local_addr().unwrap(); - - tokio::spawn(async move { - let mut client_stream = tokio::net::TcpStream::connect(addr).await.unwrap(); - client_stream - .write_all(b"SSH-2.0-Client_1.0\r\n") - .await - .unwrap(); - // Unexpected message pre-kex - client_stream.write_all(&[0, 0, 0, 2, 0, 99]).await.unwrap(); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - }); - - let (socket, _) = socket.accept().await.unwrap(); - let server = sh.new_client(socket.peer_addr().ok()); - let rs = server::run_stream(config, socket, server).await.unwrap(); - - // May not panic - assert!(rs.await.is_err()); - } - - #[derive(Clone)] - struct Server {} - - impl server::Server for Server { - type Handler = Self; - fn new_client(&mut self, _: Option) -> Self { - self.clone() - } - } - - impl server::Handler for Server { - type Error = super::Error; - } -} - -pub(crate) mod raw_no_crypto { - use std::borrow::Cow; - use std::io; - use std::sync::{Arc, Mutex, OnceLock}; - use std::time::Duration; - - use byteorder::{BigEndian, ByteOrder}; - use ssh_key::{Algorithm, PrivateKey}; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - use super::*; - - pub(crate) const MSG_SERVICE_REQUEST: u8 = 5; - pub(crate) const MSG_SERVICE_ACCEPT: u8 = 6; - pub(crate) const MSG_KEXINIT: u8 = 20; - pub(crate) const MSG_NEWKEYS: u8 = 21; - pub(crate) const MSG_USERAUTH_REQUEST: u8 = 50; - pub(crate) const MSG_USERAUTH_FAILURE: u8 = 51; - pub(crate) const MSG_USERAUTH_SUCCESS: u8 = 52; - pub(crate) const MSG_CHANNEL_OPEN: u8 = 90; - pub(crate) const MSG_CHANNEL_OPEN_CONFIRMATION: u8 = 91; - pub(crate) const MSG_CHANNEL_REQUEST: u8 = 98; - - pub(crate) async fn raw_service_request_signal( - build_payload: impl FnOnce(&mut Vec), - ) -> ServerSignal { - let mut stream = RawSession::connect().await; - let mut payload = Vec::new(); - build_payload(&mut payload); - stream.send_packet(&payload).await.unwrap(); - stream.result().await - } - - pub(crate) async fn raw_auth_request_signal( - build_payload: impl FnOnce(&mut Vec), - ) -> ServerSignal { - let mut stream = RawSession::connect().await; - stream.service_request().await.unwrap(); - - let mut payload = Vec::new(); - build_payload(&mut payload); - stream.send_packet(&payload).await.unwrap(); - stream.result().await - } - - pub(crate) async fn raw_kex_signal(build_payload: impl FnOnce(&mut Vec)) -> ServerSignal { - let mut stream = RawSession::connect_without_kex().await; - - let mut payload = Vec::new(); - build_payload(&mut payload); - stream.send_packet(&payload).await.unwrap(); - stream.result().await - } - - pub(crate) async fn raw_channel_request_signal( - build_payload: impl FnOnce(u32) -> Vec, - ) -> ServerSignal { - let mut stream = RawSession::connect().await; - stream.auth_none().await.unwrap(); - let server_channel = stream.open_session().await.unwrap(); - stream - .send_packet(&build_payload(server_channel)) - .await - .unwrap(); - stream.result().await - } - - pub(crate) struct RawSession { - pub(crate) events: Arc>>, - pub(crate) stream: tokio::net::TcpStream, - pub(crate) server_task: tokio::task::JoinHandle>, - } - - impl RawSession { - pub(crate) async fn connect() -> Self { - let mut stream = Self::connect_without_kex().await; - raw_client_no_crypto_kex(&mut stream.stream).await.unwrap(); - stream - } - - pub(crate) async fn connect_without_kex() -> Self { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - let events = Arc::new(Mutex::new(Vec::new())); - let server_events = events.clone(); - let server_task = tokio::spawn(async move { - let (socket, _) = listener.accept().await.unwrap(); - let running = - server::run_stream( - no_crypto_server_config(), - socket, - MalformedInputServer { - events: server_events, - }, - ) - .await - .unwrap(); - running.await - }); - - let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap(); - stream.write_all(b"SSH-2.0-russh-test\r\n").await.unwrap(); - read_ssh_id(&mut stream).await.unwrap(); - let _server_kex = read_packet(&mut stream).await.unwrap(); - Self { - events, - stream, - server_task, - } - } - - pub(crate) async fn service_request(&mut self) -> io::Result<()> { - let mut service = Vec::new(); - service.push(MSG_SERVICE_REQUEST); - encode_string(&mut service, b"ssh-userauth"); - self.send_packet(&service).await?; - - let accept = read_packet(&mut self.stream).await?; - assert_eq!(accept.first(), Some(&MSG_SERVICE_ACCEPT)); - Ok(()) - } - - pub(crate) async fn auth_none(&mut self) -> io::Result<()> { - self.service_request().await?; - - let mut auth = Vec::new(); - auth.push(MSG_USERAUTH_REQUEST); - encode_string(&mut auth, b"test"); - encode_string(&mut auth, b"ssh-connection"); - encode_string(&mut auth, b"none"); - self.send_packet(&auth).await?; - - let success = read_packet(&mut self.stream).await?; - assert_eq!(success.first(), Some(&MSG_USERAUTH_SUCCESS)); - Ok(()) - } - - pub(crate) async fn open_session(&mut self) -> io::Result { - let mut open = Vec::new(); - open.push(MSG_CHANNEL_OPEN); - encode_string(&mut open, b"session"); - push_u32(&mut open, 0); - push_u32(&mut open, 1024 * 1024); - push_u32(&mut open, 32 * 1024); - self.send_packet(&open).await?; - - let confirmation = read_packet(&mut self.stream).await?; - assert_eq!(confirmation.first(), Some(&MSG_CHANNEL_OPEN_CONFIRMATION)); - Ok(BigEndian::read_u32(&confirmation[5..9])) - } - - pub(crate) async fn send_packet(&mut self, payload: &[u8]) -> io::Result<()> { - self.stream.write_all(&ssh_packet(payload)).await?; - self.stream.flush().await - } - - pub(crate) async fn result(mut self) -> ServerSignal { - let result = - tokio::time::timeout(Duration::from_millis(200), &mut self.server_task).await; - let events = self.events.lock().unwrap().clone(); - - match result { - Ok(Ok(Ok(()))) => ServerSignal::Closed { events }, - Ok(Ok(Err(_error))) => ServerSignal::ProtocolError { events }, - Ok(Err(join)) if join.is_panic() => ServerSignal::Panicked { events }, - Err(_) => { - self.server_task.abort(); - ServerSignal::Survived { events } - } - _ => ServerSignal::Closed { events }, - } - } - } - - fn no_crypto_server_config() -> Arc { - let mut config = server::Config::default(); - config.inactivity_timeout = None; - config.auth_rejection_time = Duration::from_millis(1); - config.auth_rejection_time_initial = Some(Duration::from_millis(1)); - config.preferred = no_crypto_preferred(); - config - .keys - .push(PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519).unwrap()); - Arc::new(config) - } - - fn no_crypto_preferred() -> Preferred { - Preferred { - kex: Cow::Owned(vec![kex::NONE]), - key: Cow::Owned(vec![Algorithm::Ed25519]), - cipher: Cow::Owned(vec![cipher::NONE]), - mac: Cow::Owned(vec![mac::NONE]), - compression: Cow::Owned(vec![compression::NONE]), - } - } - - async fn raw_client_no_crypto_kex(stream: &mut tokio::net::TcpStream) -> io::Result<()> { - stream - .write_all(&ssh_packet(&kexinit_payload("none"))) - .await?; - let newkeys = read_packet(stream).await?; - assert_eq!(newkeys.first(), Some(&MSG_NEWKEYS)); - stream.write_all(&ssh_packet(&[MSG_NEWKEYS])).await?; - stream.flush().await - } - - pub(crate) fn pty_req_payload(server_channel: u32, terminal_modes: &[u8]) -> Vec { - let mut payload = channel_request_payload(server_channel, b"pty-req"); - encode_string(&mut payload, b"xterm"); - push_u32(&mut payload, 80); - push_u32(&mut payload, 24); - push_u32(&mut payload, 0); - push_u32(&mut payload, 0); - encode_string(&mut payload, terminal_modes); - payload - } - - pub(crate) fn channel_open_payload(channel_type: &[u8]) -> Vec { - let mut payload = Vec::new(); - encode_string(&mut payload, channel_type); - push_u32(&mut payload, 0); - push_u32(&mut payload, 1024 * 1024); - push_u32(&mut payload, 32 * 1024); - payload - } - - pub(crate) fn channel_request_payload(server_channel: u32, request_type: &[u8]) -> Vec { - let mut payload = Vec::new(); - payload.push(MSG_CHANNEL_REQUEST); - push_u32(&mut payload, server_channel); - encode_string(&mut payload, request_type); - payload.push(1); - payload - } - - pub(crate) fn kexinit_payload(kex_name: &str) -> Vec { - let mut payload = Vec::new(); - payload.push(MSG_KEXINIT); - payload.extend_from_slice(&[0; 16]); - encode_name_list(&mut payload, &[kex_name]); - encode_name_list(&mut payload, &["ssh-ed25519"]); - encode_name_list(&mut payload, &["none"]); - encode_name_list(&mut payload, &["none"]); - encode_name_list(&mut payload, &["none"]); - encode_name_list(&mut payload, &["none"]); - encode_name_list(&mut payload, &["none"]); - encode_name_list(&mut payload, &["none"]); - encode_name_list(&mut payload, &[]); - encode_name_list(&mut payload, &[]); - payload.push(0); - push_u32(&mut payload, 0); - payload - } - - fn ssh_packet(payload: &[u8]) -> Vec { - let mut padding_len = 8 - ((5 + payload.len()) % 8); - if padding_len < 4 { - padding_len += 8; - } - let packet_len = 1 + payload.len() + padding_len; - let mut packet = Vec::with_capacity(4 + packet_len); - push_u32(&mut packet, packet_len as u32); - packet.push(padding_len as u8); - packet.extend_from_slice(payload); - packet.resize(packet.len() + padding_len, 0); - packet - } - - pub(crate) async fn read_packet(stream: &mut tokio::net::TcpStream) -> io::Result> { - let mut len_buf = [0; 4]; - stream.read_exact(&mut len_buf).await?; - let packet_len = BigEndian::read_u32(&len_buf) as usize; - let mut packet = vec![0; packet_len]; - stream.read_exact(&mut packet).await?; - let padding_len = packet[0] as usize; - Ok(packet[1..packet.len() - padding_len].to_vec()) - } - - async fn read_ssh_id(stream: &mut tokio::net::TcpStream) -> io::Result> { - let mut id = Vec::new(); - loop { - let mut byte = [0]; - stream.read_exact(&mut byte).await?; - id.push(byte[0]); - if byte[0] == b'\n' { - return Ok(id); - } - } - } - - fn encode_name_list(buf: &mut Vec, names: &[&str]) { - encode_string(buf, names.join(",").as_bytes()); - } - - pub(crate) fn encode_string(buf: &mut Vec, value: &[u8]) { - push_u32(buf, value.len() as u32); - buf.extend_from_slice(value); - } - - pub(crate) fn push_u32(buf: &mut Vec, value: u32) { - let mut bytes = [0; 4]; - BigEndian::write_u32(&mut bytes, value); - buf.extend_from_slice(&bytes); - } - - pub(crate) async fn timeout( - future: impl Future, - ) -> Result { - tokio::time::timeout(Duration::from_secs(3), future).await - } - - pub(crate) async fn capture_panics(future: impl Future) -> (T, bool) { - static PANIC_HOOK_LOCK: OnceLock> = OnceLock::new(); - - let _guard = PANIC_HOOK_LOCK - .get_or_init(|| Mutex::new(())) - .lock() - .unwrap(); - let panicked = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let panicked_hook = panicked.clone(); - let previous_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |_| { - panicked_hook.store(true, std::sync::atomic::Ordering::SeqCst); - })); - - let result = future.await; - - std::panic::set_hook(previous_hook); - (result, panicked.load(std::sync::atomic::Ordering::SeqCst)) - } - - #[derive(Debug)] - pub(crate) enum ServerSignal { - Closed { events: Vec<&'static str> }, - ProtocolError { events: Vec<&'static str> }, - Panicked { events: Vec<&'static str> }, - Survived { events: Vec<&'static str> }, - } - - impl ServerSignal { - pub(crate) fn events(&self) -> &[&'static str] { - match self { - Self::Closed { events } - | Self::ProtocolError { events } - | Self::Panicked { events } - | Self::Survived { events } => events, - } - } - } - - pub(crate) fn assert_rejected( - result: Result, - message: &str, - ) { - assert!( - matches!( - result, - Ok(ServerSignal::Closed { .. } | ServerSignal::ProtocolError { .. }) - ), - "{message}: {result:?}; handler events: {:?}", - result.as_ref().ok().map(ServerSignal::events).unwrap_or(&[]) - ); - } - - #[derive(Clone)] - struct MalformedInputServer { - events: Arc>>, - } - - impl MalformedInputServer { - fn record(&self, event: &'static str) { - self.events.lock().unwrap().push(event); - } - } - - impl server::Handler for MalformedInputServer { - type Error = Error; - - async fn auth_none(&mut self, _user: &str) -> Result { - self.record("auth_none"); - Ok(server::Auth::Accept) - } - - async fn auth_password( - &mut self, - _user: &str, - _password: &str, - ) -> Result { - self.record("auth_password"); - Ok(server::Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - - async fn channel_open_session( - &mut self, - _channel: Channel, - _session: &mut server::Session, - ) -> Result { - self.record("channel_open_session"); - Ok(true) - } - - async fn pty_request( - &mut self, - _channel: ChannelId, - _term: &str, - _col_width: u32, - _row_height: u32, - _pix_width: u32, - _pix_height: u32, - _modes: &[(Pty, u32)], - _session: &mut server::Session, - ) -> Result<(), Self::Error> { - self.record("pty_request"); - Ok(()) - } - - async fn env_request( - &mut self, - _channel: ChannelId, - _variable_name: &str, - _variable_value: &str, - _session: &mut server::Session, - ) -> Result<(), Self::Error> { - self.record("env_request"); - Ok(()) - } - - async fn exec_request( - &mut self, - _channel: ChannelId, - _data: &[u8], - _session: &mut server::Session, - ) -> Result<(), Self::Error> { - self.record("exec_request"); - Ok(()) - } - - async fn signal( - &mut self, - _channel: ChannelId, - _signal: Sig, - _session: &mut server::Session, - ) -> Result<(), Self::Error> { - self.record("signal"); - Ok(()) - } - } -} - -/// Integration test for FutureCertificate authentication flow -#[cfg(unix)] -mod future_certificate { - use std::io::Write; - use std::process::Stdio; - use std::sync::Arc; - - use ssh_key::{certificate, PrivateKey}; - - use crate::keys::agent::client::AgentClient; - use crate::{client, server}; - use crate::server::Session; - - /// Helper to spawn an ssh-agent - async fn spawn_agent() -> ( - tokio::process::Child, - std::path::PathBuf, - tempfile::TempDir, - ) { - let dir = tempfile::tempdir().unwrap(); - let agent_path = dir.path().join("agent"); - let agent = tokio::process::Command::new("ssh-agent") - .arg("-a") - .arg(&agent_path) - .arg("-D") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .unwrap(); - - // Wait for the socket to be created - while agent_path.canonicalize().is_err() { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - - (agent, agent_path, dir) - } - - /// Helper to create a test certificate - fn create_test_cert(ca_key: &PrivateKey, user_key: &PrivateKey) -> ssh_key::Certificate { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let valid_after = now - 3600; - let valid_before = now + 86400 * 365; - - let mut builder = certificate::Builder::new_with_random_nonce( - &mut rand::rng(), - user_key.public_key(), - valid_after, - valid_before, - ) - .unwrap(); - - builder.serial(1).unwrap(); - builder.key_id("test-user-cert").unwrap(); - builder.cert_type(certificate::CertType::User).unwrap(); - builder.valid_principal("testuser").unwrap(); - builder.sign(ca_key).unwrap() - } - - #[tokio::test] - async fn test_future_certificate_auth_full_flow() { - let _ = env_logger::try_init(); - - // 1. Spawn ssh-agent - let (mut agent, agent_path, dir) = spawn_agent().await; - - // 2. Create CA key and user key - let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - // 3. Create a certificate - let cert = create_test_cert(&ca_key, &user_key); - - // 4. Write keys and certificate to temp files and add to agent - let user_key_path = dir.path().join("user_key"); - let cert_path = dir.path().join("user_key-cert.pub"); - - let mut f = std::fs::File::create(&user_key_path).unwrap(); - f.write_all( - user_key - .to_openssh(ssh_key::LineEnding::LF) - .unwrap() - .as_bytes(), - ) - .unwrap(); - std::fs::set_permissions( - &user_key_path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .unwrap(); - - let mut f = std::fs::File::create(&cert_path).unwrap(); - f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap(); - - let status = tokio::process::Command::new("ssh-add") - .arg(&user_key_path) - .env("SSH_AUTH_SOCK", &agent_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .unwrap(); - assert!(status.success(), "ssh-add failed"); - - // 5. Set up test server that accepts certificate auth - let mut server_config = server::Config::default(); - server_config.inactivity_timeout = None; - server_config.auth_rejection_time = std::time::Duration::from_secs(3); - server_config - .keys - .push(PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap()); - let server_config = Arc::new(server_config); - - let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = socket.local_addr().unwrap(); - - // Server that accepts certificate auth - let server_join = tokio::spawn(async move { - let (socket, _) = socket.accept().await.unwrap(); - - struct CertHandler; - - impl server::Handler for CertHandler { - type Error = crate::Error; - - async fn auth_publickey_offered( - &mut self, - _user: &str, - _public_key: &ssh_key::PublicKey, - ) -> Result { - // Accept the key/certificate for the probe - Ok(server::Auth::Accept) - } - - async fn auth_openssh_certificate( - &mut self, - _user: &str, - cert: &ssh_key::Certificate, - ) -> Result { - // Validate the certificate is signed by our CA - // In a real server, you'd properly verify the CA signature - // For this test, just check the key_id matches - if cert.key_id() == "test-user-cert" { - Ok(server::Auth::Accept) - } else { - Ok(server::Auth::Reject { proceed_with_methods: None, partial_success: false }) - } - } - - async fn channel_open_session( - &mut self, - channel: crate::Channel, - _session: &mut Session, - ) -> Result { - drop(channel); - Ok(true) - } - } - - let handler = CertHandler; - server::run_stream(server_config, socket, handler) - .await - .unwrap() - }); - - // 6. Connect as client using FutureCertificate auth with the agent - let client_config = Arc::new(client::Config::default()); - - struct TestClient; - impl client::Handler for TestClient { - type Error = crate::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &ssh_key::PublicKey, - ) -> Result { - Ok(true) - } - } - - let mut session = client::connect(client_config, addr, TestClient) - .await - .unwrap(); - - // Connect to the agent - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); - let mut agent_client = AgentClient::connect(stream); - - // Authenticate using FutureCertificate (None for hash_alg since Ed25519 doesn't need it) - let auth_result = session - .authenticate_certificate_with("testuser", cert.clone(), None, &mut agent_client) - .await - .unwrap(); - - // 7. Verify authentication succeeded - assert!(auth_result.success(), "Certificate authentication should succeed"); - - // Clean up - session.disconnect(crate::Disconnect::ByApplication, "", "").await.unwrap(); - drop(session); - server_join.abort(); - agent.kill().await.unwrap(); - agent.wait().await.unwrap(); - } -} diff --git a/crates/bssh-russh/sync-upstream.sh b/crates/bssh-russh/sync-upstream.sh deleted file mode 100755 index 9b798a7e..00000000 --- a/crates/bssh-russh/sync-upstream.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/bin/bash -# sync-upstream.sh -# Syncs bssh-russh with upstream russh and applies our patches -# -# Usage: ./sync-upstream.sh [version] -# version: optional, e.g., "0.56.0" or "main" (default: latest tag) - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -UPSTREAM_URL="https://github.com/warp-tech/russh.git" -TEMP_DIR="/tmp/russh-sync-$$" -PATCH_DIR="$SCRIPT_DIR/patches" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } - -cleanup() { - if [ -d "$TEMP_DIR" ]; then - rm -rf "$TEMP_DIR" - fi -} -trap cleanup EXIT - -# Parse arguments -VERSION="${1:-}" - -log_info "Syncing bssh-russh with upstream russh..." - -# Clone upstream -log_info "Cloning upstream russh..." -git clone --depth 100 "$UPSTREAM_URL" "$TEMP_DIR" - -cd "$TEMP_DIR" - -# Determine version -if [ -z "$VERSION" ]; then - VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "main") - log_info "Using latest tag: $VERSION" -elif [ "$VERSION" != "main" ]; then - log_info "Using specified version: $VERSION" -fi - -# Checkout version -if [ "$VERSION" != "main" ]; then - git checkout "v$VERSION" 2>/dev/null || git checkout "$VERSION" -fi - -COMMIT_HASH=$(git rev-parse --short HEAD) -log_info "Upstream commit: $COMMIT_HASH" - -# Copy russh source files -log_info "Copying source files..." -cd "$SCRIPT_DIR" - -# Preserve our Cargo.toml and README.md -cp Cargo.toml Cargo.toml.bak -cp README.md README.md.bak 2>/dev/null || true - -# Remove old source (except patches directory and scripts) -find src -type f -name "*.rs" -delete 2>/dev/null || true - -# Copy new source from upstream -cp -r "$TEMP_DIR/russh/src/"* src/ - -# Restore our files -mv Cargo.toml.bak Cargo.toml -mv README.md.bak README.md 2>/dev/null || true - -# Update version in Cargo.toml -if [ "$VERSION" != "main" ]; then - CLEAN_VERSION="${VERSION#v}" - sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml - log_info "Updated version to $CLEAN_VERSION" -fi - -# Apply our patches -# -# Each *.patch file under patches/ is a forward-port of a fix that is either -# unique to this fork (e.g. handle-data-fix.patch) or a cherry-pick of an -# unreleased upstream commit. For cherry-picks, once upstream releases a -# version that includes the change, the next sync will automatically detect -# it (reverse-apply succeeds) and skip the patch — at which point the patch -# file should be deleted. -log_info "Applying patches..." - -shopt -s nullglob -PATCH_FILES=("$PATCH_DIR"/*.patch) -shopt -u nullglob - -if [ ${#PATCH_FILES[@]} -eq 0 ]; then - log_warn "No patch files found in $PATCH_DIR/" -fi - -OBSOLETE_PATCHES=() - -for PATCH_FILE in "${PATCH_FILES[@]}"; do - PATCH_NAME=$(basename "$PATCH_FILE") - - # If reverse-apply succeeds, the change is already in upstream — skip and - # mark the patch as obsolete so the maintainer can delete it. - if patch -p1 -R --dry-run --silent < "$PATCH_FILE" > /dev/null 2>&1; then - log_info "Skipping $PATCH_NAME — already present in upstream (consider deleting this patch file)" - OBSOLETE_PATCHES+=("$PATCH_NAME") - continue - fi - - if patch -p1 --dry-run --silent < "$PATCH_FILE" > /dev/null 2>&1; then - patch -p1 --silent < "$PATCH_FILE" - log_info "Applied $PATCH_NAME" - else - log_warn "$PATCH_NAME may not apply cleanly, attempting with fuzz..." - if patch -p1 --fuzz=3 < "$PATCH_FILE"; then - log_warn "$PATCH_NAME applied with fuzz - please verify manually" - else - log_error "Failed to apply $PATCH_NAME. Manual intervention required." - log_error "Patch file: $PATCH_FILE" - exit 1 - fi - fi -done - -# Verify build -log_info "Verifying build..." -cd "$SCRIPT_DIR/../.." -if cargo check -p bssh-russh 2>/dev/null; then - log_info "Build verification passed" -else - log_error "Build verification failed" - exit 1 -fi - -log_info "Sync complete!" -log_info "Upstream version: $VERSION ($COMMIT_HASH)" - -if [ ${#OBSOLETE_PATCHES[@]} -gt 0 ]; then - log_info "" - log_warn "The following patches are now obsolete (already in upstream $VERSION):" - for p in "${OBSOLETE_PATCHES[@]}"; do - log_warn " - $p" - done - log_warn "Delete these patch files: rm $PATCH_DIR/{$(IFS=,; echo "${OBSOLETE_PATCHES[*]}")}" -fi - -log_info "" -log_info "Next steps:" -log_info " 1. Review changes: git diff crates/bssh-russh/" -log_info " 2. Test: cargo test -p bssh-russh" -log_info " 3. Commit: git add -A && git commit -m 'chore: sync bssh-russh with upstream $VERSION'" diff --git a/crates/bssh-russh/tests/pty_handle_data.rs b/crates/bssh-russh/tests/pty_handle_data.rs deleted file mode 100644 index a4a43398..00000000 --- a/crates/bssh-russh/tests/pty_handle_data.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Regression test for the BSSH PTY fix re-ported onto russh 0.61.1. -//! -//! The fork exists to fix one problem: shell/PTY output written via -//! `Handle::data()` / `Channel::data()` from a task *other than* the server -//! session loop could fail to be delivered promptly, because the loop's -//! `tokio::select!` didn't always wake for the internal mpsc channel. The fix -//! is the `try_recv` batch-drain loop in `src/server/session.rs` that flushes -//! pending channel messages before entering `select!`. -//! -//! This test drives a real client<->server pair over a TCP loopback and floods -//! a large number of data chunks from a task distinct from the session loop, -//! then asserts every byte arrives intact. If the drain path ever loses or -//! stalls messages, `wait_for` hits its deadline and the test fails. - -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Duration; - -use russh::keys::PrivateKeyWithHashAlg; -use russh::keys::ssh_key::{self, PrivateKey}; -use russh::server::{self, Auth, Session}; -use russh::{ChannelId, client}; - -const CHUNK: usize = 1024; -const N_CHUNKS: usize = 2000; -const TOTAL: usize = CHUNK * N_CHUNKS; -const FILL: u8 = b'x'; - -/// Client handler: tallies received bytes and flags any byte that isn't `FILL`. -struct ClientH { - received: Arc, - corrupt: Arc, -} - -impl client::Handler for ClientH { - type Error = russh::Error; - - async fn check_server_key(&mut self, _key: &ssh_key::PublicKey) -> Result { - Ok(true) - } - - async fn data( - &mut self, - _channel: ChannelId, - data: &[u8], - _session: &mut client::Session, - ) -> Result<(), Self::Error> { - if data.iter().any(|&b| b != FILL) { - self.corrupt.fetch_add(1, Ordering::SeqCst); - } - self.received.fetch_add(data.len(), Ordering::SeqCst); - Ok(()) - } -} - -/// Server handler: accept any pubkey and signal once authentication finishes. -struct ServerH { - auth_tx: Option>, -} - -impl server::Handler for ServerH { - type Error = russh::Error; - - async fn auth_publickey( - &mut self, - _user: &str, - _key: &ssh_key::PublicKey, - ) -> Result { - Ok(Auth::Accept) - } - - async fn auth_succeeded(&mut self, _session: &mut Session) -> Result<(), Self::Error> { - if let Some(tx) = self.auth_tx.take() { - let _ = tx.send(()); - } - Ok(()) - } -} - -/// Poll `counter` until it reaches `target`, panicking after a generous deadline -/// (a stalled/lossy drain path manifests here as a timeout). -async fn wait_for(counter: &AtomicUsize, target: usize, label: &str) { - let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - loop { - let cur = counter.load(Ordering::SeqCst); - if cur >= target { - return; - } - assert!( - tokio::time::Instant::now() < deadline, - "timeout waiting for {label}: {cur}/{target} bytes" - ); - tokio::time::sleep(Duration::from_millis(5)).await; - } -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn high_throughput_handle_data_from_task_is_fully_delivered() { - let _ = env_logger::try_init(); - - let received = Arc::new(AtomicUsize::new(0)); - let corrupt = Arc::new(AtomicUsize::new(0)); - - let client_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap(); - - let mut config = server::Config::default(); - config.inactivity_timeout = None; - config.auth_rejection_time = Duration::from_secs(3); - config - .keys - .push(PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap()); - let config = Arc::new(config); - - let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = socket.local_addr().unwrap(); - - let (auth_tx, auth_rx) = tokio::sync::oneshot::channel(); - - // Server side: accept the connection and run the session loop (detached). - let server_join = tokio::spawn(async move { - let (stream, _) = socket.accept().await.unwrap(); - server::run_stream( - config, - stream, - ServerH { - auth_tx: Some(auth_tx), - }, - ) - .await - .map_err(|_| ()) - .unwrap() - }); - - // Client side: connect + authenticate. - let received_c = received.clone(); - let corrupt_c = corrupt.clone(); - let client_join = tokio::spawn(async move { - let cfg = Arc::new(client::Config::default()); - let mut session = client::connect( - cfg, - addr, - ClientH { - received: received_c, - corrupt: corrupt_c, - }, - ) - .await - .map_err(|_| ()) - .unwrap(); - let authed = session - .authenticate_publickey( - "user", - PrivateKeyWithHashAlg::new(Arc::new(client_key), None), - ) - .await - .unwrap(); - assert!(authed.success(), "client authentication failed"); - session - }); - - let (server_session, client_session) = tokio::join!(server_join, client_join); - // `RunningSession::handle()` keeps the detached session loop reachable; the - // client `Handle` must stay alive so the connection isn't torn down. - let handle = server_session.unwrap().handle(); - let _client = client_session.unwrap(); - - // The whole exchange must finish well within this bound. - tokio::time::timeout(Duration::from_secs(45), async { - auth_rx.await.unwrap(); - - // Flood data from a task that is NOT the session loop — the fork's bug - // scenario. `Channel::data()` enqueues onto the session loop's mpsc; the - // drain loop is what must deliver all of it. - let received_s = received.clone(); - let flood = tokio::spawn(async move { - let ch = handle.channel_open_session().await.unwrap(); - let chunk = vec![FILL; CHUNK]; - for _ in 0..N_CHUNKS { - ch.data(&chunk[..]).await.unwrap(); - } - // Hold the channel open until the client has accounted for everything. - wait_for(&received_s, TOTAL, "server-side confirmation").await; - ch - }); - - wait_for(&received, TOTAL, "client receive").await; - let _ch = flood.await.unwrap(); - }) - .await - .expect("PTY data exchange timed out — drain loop did not deliver all messages"); - - assert_eq!( - received.load(Ordering::SeqCst), - TOTAL, - "expected exactly {TOTAL} bytes delivered" - ); - assert_eq!( - corrupt.load(Ordering::SeqCst), - 0, - "received corrupted/misordered chunk(s)" - ); - - drop(_client); -} diff --git a/src/server/handler.rs b/src/server/handler.rs index f4048413..694aa18a 100644 --- a/src/server/handler.rs +++ b/src/server/handler.rs @@ -23,7 +23,7 @@ use std::sync::Arc; use futures::FutureExt; use russh::keys::ssh_key; -use russh::server::{Auth, Msg, Session}; +use russh::server::{Auth, ChannelOpenHandle, Msg, Session}; use russh::{Channel, ChannelId, MethodKind, MethodSet, Pty}; use tokio::sync::RwLock; use zeroize::Zeroizing; @@ -254,8 +254,9 @@ impl russh::server::Handler for SshHandler { fn channel_open_session( &mut self, channel: Channel, + reply: ChannelOpenHandle, _session: &mut Session, - ) -> impl std::future::Future> + Send { + ) -> impl std::future::Future> + Send { let channel_id = channel.id(); tracing::debug!( peer = ?self.peer_addr, @@ -266,7 +267,10 @@ impl russh::server::Handler for SshHandler { // Store the channel itself so we can use it for subsystems like SFTP self.channels .insert(channel_id, ChannelState::with_channel(channel)); - async { Ok(true) } + async move { + reply.accept().await; + Ok(()) + } } /// Handle 'none' authentication.