diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000000000000000000000000000000000000..a58ec5754d6a3aabcc71e7d07a284c03a8e072c7 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,6 @@ +max_width = 85 +use_small_heuristics = "Max" +fn_args_layout = "Compressed" +# unstable +# trailing_semicolon = false +# overflow_delimited_expr = true diff --git a/Cargo.lock b/Cargo.lock index 97fcfdf2e060c7fd30412a9dc791010078a6649b..1788f81db79e1613c713527fad10c85a800fb7bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,130 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "anyhow" version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d14f329cfbaf5d0e06b5e87fff7e265d2673c5ea7d2c27691a2c107db1442a0" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -22,10 +140,51 @@ version = "0.1.0" name = "door-sshproto" version = "0.1.0" dependencies = [ + "aes", "anyhow", + "ctr", + "hmac", + "log", + "pretty-hex", + "rand", + "ring", "serde", + "sha2", + "snafu", +] + +[[package]] +name = "door-tokio" +version = "0.1.0" +dependencies = [ + "anyhow", + "door-sshproto", + "log", + "pretty-hex", + "simplelog", "snafu", - "toml", + "tokio", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] @@ -37,6 +196,181 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pretty-hex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" + [[package]] name = "proc-macro2" version = "1.0.37" @@ -55,6 +389,54 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.136" @@ -75,6 +457,43 @@ dependencies = [ "syn", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "simplelog" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1348164456f72ca0116e4538bdaabb0ddb622c7d9f16387c725af3e96d6001c" +dependencies = [ + "chrono", + "log", + "termcolor", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + [[package]] name = "snafu" version = "0.7.0" @@ -97,6 +516,28 @@ dependencies = [ "syn", ] +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.91" @@ -109,14 +550,61 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.5.8" +name = "termcolor" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ - "serde", + "winapi-util", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + [[package]] name = "unicode-segmentation" version = "1.9.0" @@ -128,3 +616,165 @@ name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" diff --git a/Cargo.toml b/Cargo.toml index 84253fe120f48d6f3e64185be3e954f0cd31288a..cf6fe077f1362943aa11c5efa51493110cc41444 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,21 @@ name = "door" version = "0.1.0" edition = "2021" +license = "MPL-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = ["door-sshproto"] +members = [ + "door-sshproto", + "door-tokio", +] -[dependencies] +[profile.release] +opt-level = 's' +lto = "fat" +debug = 1 + +[patch.crates-io] +# serde_state = { version = "0.4", path = "../../3rd/serde_state/serde_state" } +# serde_derive_state = { version = "0.4", path = "../../3rd/serde_state/serde_derive" } diff --git a/door-sshproto/Cargo.toml b/door-sshproto/Cargo.toml index e9f31e73b43933b166cc920520b9701782bf6635..0c409a47c5bcc9bc253e609538c5d4753720c063 100644 --- a/door-sshproto/Cargo.toml +++ b/door-sshproto/Cargo.toml @@ -3,19 +3,36 @@ name = "door-sshproto" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -# snafu = { version = "0.7", default-features = false } -snafu = { version = "0.7" } -# nom = { version = "7.1", default-features = false } serde = { version = "1.0", default-features = false, features = ["derive"]} +snafu = { version = "0.7", default-features = false } +# TODO: check that log macro calls disappear in no_std builds +log = { version = "0.4" } +# TODO: probably needs changing for embedded platforms +rand = { version = "0.8", default-features = false, features = ["getrandom"] } + +ring = { version = "0.16", default-features = false } +# for aes-ctr mode +ctr = "0.9" +aes = "0.8" +sha2 = "0.10" +hmac = "0.12" + +pretty-hex = "0.2" + +[features] +std = [] + +# serde_state = { version = "0.4", default-features = false, features = ["derive"]} +# serde_derive_state = { version = "0.4" } [dev-dependencies] -toml = "0.5" +# toml = "0.5" # examples want std::error snafu = { version = "0.7", default-features = true } anyhow = { version = "1.0" } +pretty-hex = "0.2" - +[[example]] +name = "kex1" diff --git a/door-sshproto/LICENSE b/door-sshproto/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f212b3d46a17aa0ef0c10a82f924c33856d77e26 --- /dev/null +++ b/door-sshproto/LICENSE @@ -0,0 +1,377 @@ +Door SSH is (c) 2022 Matt Johnston and contributors +Provided under terms of the MPL 2.0 as below. + + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/door-sshproto/examples/kex1.rs b/door-sshproto/examples/kex1.rs index 3db7c687dba8439ab5e9db9de9623ebcd99d015c..242ddca34c14aa793e730aba0976c7bfc5365781 100644 --- a/door-sshproto/examples/kex1.rs +++ b/door-sshproto/examples/kex1.rs @@ -1,15 +1,20 @@ -use anyhow::{Result, Error, Context}; +#![allow(unused_imports)] -use door_sshproto::packets::KexInit; +use anyhow::{Context, Error, Result}; +use pretty_hex::PrettyHex; + +use door_sshproto::packets::*; +use door_sshproto::wireformat::BinString; fn main() -> Result<(), Error> { let k = KexInit { - cookie: &[1,2,3], + // cookie: &[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], + cookie: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], kex: "hello,more".into(), hostkey: "hello,more".into(), - enc_c2s: "hello,more".into(), - enc_s2c: "hello,more".into(), - mac_c2s: "hello,more".into(), + cipher_c2s: "hello,more".into(), + cipher_s2c: "hello,more".into(), + mac_c2s: "hi".into(), mac_s2c: "hello,more".into(), comp_c2s: "hello,more".into(), comp_s2c: "hello,more".into(), @@ -18,15 +23,38 @@ fn main() -> Result<(), Error> { first_follows: false, reserved: 0, }; - println!("kex1 {k:?}"); - let t = toml::to_string(&k)?; - println!("as toml:\n{}", t); + let p = Packet::KexInit(k); + println!("p {p:?}"); + + let bs = BinString(&[0x11, 0x22, 0x33]); + let dhc = + Packet::KexDHInit(KexDHInit::Curve25519Init(Curve25519Init { q_c: bs })); + println!("dhc1 {dhc:?}"); + + // if let SpecificPacket::KexInit(ref k2) = p.p { + // let t = toml::to_string(&k2)?; + // println!("as toml:\n{}", t); + + // } // let k2: KexInit = toml::from_str(&t).context("deser")?; // println!("kex2 {k:?}"); let mut buf = vec![0; 2000]; - let written = door_sshproto::wireformat::write_ssh(&mut buf, &k)?; + let written = door_sshproto::wireformat::write_ssh(&mut buf, &p)?; buf.truncate(written); - println!("buf is {buf:#?}"); + println!("{:?}", buf.hex_dump()); + let ctx = door_sshproto::packets::ParseContext::new(); + let x: Packet = door_sshproto::wireformat::packet_from_bytes(&buf, &ctx)?; + println!("fetched {x:?}"); + + // let mut buf = vec![0; 2000]; + // let written = door_sshproto::wireformat::write_ssh(&mut buf, &dhc)?; + // buf.truncate(written); + // println!("wrote {written} {:?}", buf.hex_dump()); + // let mut ctx = door_sshproto::packets::ParseContext::new(); + // ctx.kextype = KexType::Curve25519; + // let x: Packet = door_sshproto::wireformat::packet_from_bytes(&buf, ctx)?; + // println!("fetched {:?}", buf.hex_dump()); + // println!("fetched {x:?} {:p}", &x); Ok(()) } diff --git a/door-sshproto/src/conn.rs b/door-sshproto/src/conn.rs new file mode 100644 index 0000000000000000000000000000000000000000..2ecbef7dbb5771fc44972024f5fa7975a706c1ef --- /dev/null +++ b/door-sshproto/src/conn.rs @@ -0,0 +1,129 @@ +#[allow(unused_imports)] +use { + crate::error::Error, + log::{debug, error, info, log, trace, warn}, +}; + +use ring::digest::Digest; + +use crate::*; +use encrypt::KeyState; +use packets::Packet; +use traffic::Traffic; + +pub struct Runner<'a> { + conn: Conn<'a>, + /// Binary packet handling + traffic: Traffic<'a>, +} + +impl<'a> Runner<'a> { + pub fn new(conn: Conn<'a>, iobuf: &'a mut [u8]) -> Self { + Runner { + conn, + traffic: traffic::Traffic::new(iobuf), + } + } + + pub fn input(&mut self, buf: &[u8]) -> Result<usize, Error> { + let (size, payload) = self.traffic.input(&mut self.conn.keys, &mut self.conn.remote_version, buf)?; + if let Some(payload) = payload { + if matches!(self.conn.state, ConnState::Ident) { + self.conn.state = ConnState::PreKex; + } + self.conn.handle_payload(payload)? + } + Ok(size) + } + +} + +/// The core state of a SSH instance. +pub struct Conn<'a> { + state: ConnState, + + /// In-progress kex state + kex: Option<kex::Kex>, + + /// Current encryption/integrity keys + keys: KeyState, + + /// TODO: Digest is sized to fit 512 bits, we only need 256 for ours currently? + sess_id: Option<Digest>, + + pub(crate) is_client: bool, + + pub(crate) algo_conf: kex::AlgoConfig<'a>, + + /// Remote version string. Kept for later kexinit rekeying + pub(crate) remote_version: ident::RemoteVersion, + + parse_ctx: packets::ParseContext, +} + +enum ConnState { + /// Prior to SSH binary packet protocol, receiving remote version identification + Ident, + /// Binary packet protocol has started, KexInit not yet received + PreKex, + /// At any time between receiving KexInit and NewKeys. Can occur multiple times + /// at later key exchanges + InKex, + /// After first NewKeys, prior to auth success + PreAuth, + /// After auth success + Auth, + // Cleanup ?? +} + +impl<'a> Conn<'a> { + /// [`iobuf`] must be sized to fit the largest SSH packet allowed + pub fn new() -> Self { + let is_client = true; // TODO= true; + Conn { + kex: None, + keys: KeyState::new_cleartext(), + sess_id: None, + remote_version: ident::RemoteVersion::new(), + parse_ctx: packets::ParseContext::new(), + state: ConnState::Ident, + algo_conf: kex::AlgoConfig::new(is_client), + is_client, + } + } + + /// Consumes an input payload + pub(crate) fn handle_payload(&mut self, payload: &[u8]) -> Result<(), Error> { + if matches!(self.state, ConnState::Ident) { + return Err(Error::Bug); + } + self.keys.next_seq_decrypt(); + trace!("bef"); + let p = wireformat::packet_from_bytes(payload, &self.parse_ctx)?; + trace!("handle_payload() got {p:#?}"); + self.dispatch_packet(&p)?; + Ok(()) + } + + fn dispatch_packet(&mut self, packet: &Packet) -> Result<(), Error> { + // TODO: perhaps could consolidate packet allowed checks into a separate function + // to run first? + match packet { + Packet::KexInit(p) => { + if matches!(self.state, ConnState::InKex) { + return Err(Error::PacketWrong); + } + self.kex.get_or_insert_with(kex::Kex::new).handle_kexinit( + self.is_client, + &self.algo_conf, + &self.remote_version, + p, + ) + } + p => { + warn!("Unhandled packet {p:?}"); + Err(Error::UnknownPacket) + } + } + } +} diff --git a/door-sshproto/src/encrypt.rs b/door-sshproto/src/encrypt.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a06d2d7f68461af4703dc8bcb7074b930c586d3 --- /dev/null +++ b/door-sshproto/src/encrypt.rs @@ -0,0 +1,471 @@ +#[allow(unused_imports)] +use { + crate::error::Error, + log::{debug, error, info, log, trace, warn}, +}; + +use core::num::Wrapping; +use ring::aead::chacha20_poly1305_openssh as chapoly; +use aes::cipher::{KeyIvInit, KeySizeUser, BlockSizeUser, StreamCipher}; +use hmac::{Hmac, Mac}; +use sha2::Digest; + +use crate::kex; +use crate::*; + +// TODO: check that Ctr32 is sufficient. Should be OK with SSH rekeying. +type Aes256Ctr32BE = ctr::Ctr32BE<aes::Aes256>; +type HmacSha256 = hmac::Hmac<sha2::Sha256>; + + +// RFC4253 Section 6. Including length u32 length field, excluding MAC +const SSH_MIN_PACKET_SIZE: usize = 16; +const SSH_MIN_PADLEN: usize = 4; +const SSH_MIN_BLOCK: usize = 8; +pub const SSH_LENGTH_SIZE: usize = 4; + +/// Stateful [`Keys`], stores a sequence number as well +pub(crate) struct KeyState { + keys: Keys, + // Packet sequence numbers. These must be transferred to subsequent KeyState + // since they don't reset with rekeying. + seq_encrypt: Wrapping<u32>, + seq_decrypt: Wrapping<u32>, +} + +impl KeyState { + /// A brand new `KeyState` with no encryption, zero sequence numbers + pub fn new_cleartext() -> Self { + KeyState { + keys: Keys::new_cleartext(), + seq_encrypt: Wrapping(0), + seq_decrypt: Wrapping(0), + } + } + + pub fn next_seq_decrypt(&mut self) { + self.seq_decrypt += 1; + } + + /// Decrypts the first block in the buffer, returning the length. + pub fn decrypt_first_block(&mut self, buf: &mut [u8]) -> Result<u32, Error> { + self.keys.decrypt_first_block(buf, self.seq_decrypt.0) + } + + /// Decrypt bytes 4 onwards of the buffer and validate AEAD Tag or MAC. + /// Ensures that the packet meets minimum length. + pub fn decrypt<'b>(&mut self, buf: &'b mut [u8]) -> Result<(), Error> { + self.keys.decrypt(buf, self.seq_decrypt.0) + } + + /// [`buf`] is the entire output buffer to encrypt in place. + /// payload_len is the length of the payload portion + /// This is stateful, updating the sequence number. + pub fn encrypt<'b>( + &mut self, payload_len: usize, buf: &'b mut [u8], + ) -> Result<usize, Error> { + let e = self.keys.encrypt(payload_len, buf, self.seq_encrypt.0); + self.seq_encrypt += 1; + e + } + pub fn size_integ_enc(&self) -> usize { + self.keys.integ_enc.size_out() + } + pub fn size_integ_dec(&self) -> usize { + self.keys.integ_dec.size_out() + } + pub fn size_block_enc(&self) -> usize { + self.keys.enc.size_block() + } + pub fn size_block_dec(&self) -> usize { + self.keys.dec.size_block() + } +} + +pub(crate) struct Keys { + pub(crate) enc: EncKey, + pub(crate) dec: DecKey, + + pub(crate) integ_enc: IntegKey, + pub(crate) integ_dec: IntegKey, +} + +impl Keys { + // pub(crate) fn new( + // enc: EncKey, dec: DecKey, integ_enc: IntegKey, integ_dec: IntegKey, + // ) -> Self { + // Keys { enc, dec, integ_enc, integ_dec } + // } + + pub fn new_cleartext() -> Self { + Keys { + enc: EncKey::NoCipher, + dec: DecKey::NoCipher, + integ_enc: IntegKey::NoInteg, + integ_dec: IntegKey::NoInteg, + } + } + + /// Decrypts the first block in the buffer, returning the length. + /// Whether bytes `buf[4..block_size]` are decrypted depends on the cipher, they may be + /// handled later by [`decrypt`]. Bytes `buf[0..4]` may not be modified. + pub fn decrypt_first_block( + &mut self, buf: &mut [u8], seq: u32, + ) -> Result<u32, Error> { + if buf.len() < self.dec.size_block() { + return Err(Error::Bug); + } + let buf4: [u8; 4] = buf[0..4].try_into().unwrap(); + + let d4 = match &mut self.dec { + DecKey::ChaPoly(openkey) => { + openkey.decrypt_packet_length(seq, buf4); + &buf4 + } + DecKey::Aes256Ctr(a) => { + a.apply_keystream(&mut buf[..16]); + buf[..4].try_into().unwrap() + } + DecKey::NoCipher => &buf4, + }; + Ok(u32::from_be_bytes(*d4)) + } + + /// Decrypt bytes 4 onwards of the buffer and validate AEAD Tag or MAC. + /// Ensures that the packet meets minimum length. + /// The first block_size bytes may have been already decrypted by + /// [`decrypt_first_block`] + /// depending on the cipher. + pub fn decrypt(&mut self, buf: &mut [u8], seq: u32) -> Result<(), Error> { + let size_block = self.dec.size_block(); + let size_integ = self.integ_dec.size_out(); + if buf.len() < size_block { + return Err(Error::BadDecrypt); + } + if 4 + buf.len() - size_integ < SSH_MIN_PACKET_SIZE { + return Err(Error::SSHProtoError); + } + // "MUST be a multiple of the cipher block size". + // encrypted length for aead ciphers doesn't include the length prefix. + let len = if self.dec.is_aead() { 0 } else { SSH_LENGTH_SIZE } + buf.len() + - size_integ; + + if len % size_block != 0 { + return Err(Error::SSHProtoError); + } + + let (data, mac) = buf.split_at_mut(buf.len() - size_integ); + + // TODO: ETM modes would check integrity here. + + match &mut self.dec { + DecKey::ChaPoly(openkey) => { + let mac: &mut [u8; chapoly::TAG_LEN] = + mac.try_into().map_err(|_| Error::Bug)?; + + openkey + .open_in_place(seq, data, mac) + .map_err(|_| Error::BadDecrypt)?; + } + DecKey::Aes256Ctr(a) => { + a.apply_keystream(data); + } + DecKey::NoCipher => {} + } + + match self.integ_dec { + IntegKey::ChaPoly => {} + IntegKey::NoInteg => {} + IntegKey::HmacSha256(k) => { + // new_from_slice can't fail. + let mut h = HmacSha256::new_from_slice(&k).unwrap(); + h.update(data); + h.verify_slice(mac) + .map_err(|_| Error::BadDecrypt)?; + } + } + Ok(()) + } + + /// Padding is required to meet + /// - minimum packet length + /// - minimum padding size, + /// - encrypted length being a multiple of block length + fn get_encrypt_pad(&self, payload_len: usize) -> usize { + let size_block = self.enc.size_block(); + let size_integ = self.integ_enc.size_out(); + // aead ciphers don't include the initial length field in encrypted blocks + let len = + 1 + payload_len + if self.enc.is_aead() { 0 } else { SSH_LENGTH_SIZE }; + + // round padding length upwards so that len is a multiple of block size + let mut padlen = self.enc.size_block() - len % self.enc.size_block(); + + // need at least 4 bytes padding + if padlen < SSH_MIN_PADLEN { + padlen += self.enc.size_block() + } + + // The minimum size of a packet is 16 (plus mac) + // We know we already have at least 8 bytes because of blocksize rounding. + if SSH_LENGTH_SIZE + 1 + payload_len + padlen < SSH_MIN_PACKET_SIZE { + padlen += self.enc.size_block() + } + padlen + } + + /// Encrypt a buffer in-place, adding packet size, padding, MAC etc. + /// Returns the total length. + /// Ensures that the packet meets minimum and other length requirements. + pub fn encrypt( + &mut self, payload_len: usize, buf: &mut [u8], seq: u32, + ) -> Result<usize, Error> { + let size_block = self.enc.size_block(); + let size_integ = self.integ_enc.size_out(); + let padlen = self.get_encrypt_pad(payload_len); + // len is everything except the MAC + let len = SSH_LENGTH_SIZE + 1 + payload_len + padlen; + + if self.enc.is_aead() { + debug_assert_eq!((len - SSH_LENGTH_SIZE) % size_block, 0); + } else { + debug_assert_eq!(len % size_block, 0); + }; + + if len + size_integ > buf.len() { + error!("Output buffer {} is too small for packet", buf.len()); + return Err(Error::Bug); + } + + // write the length + buf[0..SSH_LENGTH_SIZE] + .copy_from_slice(&((len - SSH_LENGTH_SIZE) as u32).to_be_bytes()); + // write random padding + let pad_start = SSH_LENGTH_SIZE+1+payload_len; + debug_assert_eq!(pad_start+padlen, len); + random::fill_random(&mut buf[pad_start..pad_start+padlen]); + + let (enc, rest) = buf.split_at_mut(len); + let (mac, _) = rest.split_at_mut(size_integ); + + match self.integ_enc { + IntegKey::ChaPoly => {} + IntegKey::NoInteg => {} + IntegKey::HmacSha256(k) => { + // new_from_slice can't fail. + let mut h = HmacSha256::new_from_slice(&k).unwrap(); + h.update(enc); + let result = h.finalize(); + mac.copy_from_slice(&result.into_bytes()); + } + } + + match &mut self.enc { + EncKey::ChaPoly(sealkey) => { + let mac: &mut [u8; chapoly::TAG_LEN] = + mac.try_into().map_err(|_| Error::Bug)?; + + sealkey.seal_in_place(seq, enc, mac); + } + EncKey::Aes256Ctr(a) => { + a.apply_keystream(enc); + } + EncKey::NoCipher => {} + } + + // ETM modes would go here. + + Ok(len + size_integ) + } +} + +/// Placeholder for a cipher type prior to creating a a [`EncKey`] or [`DecKey`], +/// for use during key setup in [`kex`] +pub(crate) enum Cipher { + ChaPoly, + Aes256Ctr, + // TODO Aes gcm etc +} + +impl Cipher { + /// Creates a cipher key by algorithm name. Must be passed a known name. + pub fn from_name(name: &str) -> Result<Self, Error> { + use crate::kex::*; + match name { + SSH_NAME_CHAPOLY => Ok(Cipher::ChaPoly), + SSH_NAME_SSH_NAME_AES256_CTR => Ok(Cipher::Aes256Ctr), + _ => Err(Error::Bug), + } + } + pub fn key_len(&self) -> usize { + match self { + Cipher::ChaPoly => chapoly::KEY_LEN, + Cipher::Aes256Ctr => aes::Aes256::key_size(), + } + } + pub fn iv_len(&self) -> usize { + match self { + Cipher::ChaPoly => 0, + Cipher::Aes256Ctr => aes::Aes256::block_size(), + } + } + /// Returns the [`Integ`] for this cipher, or None if not aead + pub fn integ(&self) -> Option<Integ> { + match self { + Cipher::ChaPoly => Some(Integ::ChaPoly), + Cipher::Aes256Ctr => None, + } + } +} + +pub(crate) enum EncKey { + ChaPoly(chapoly::SealingKey), + Aes256Ctr(Aes256Ctr32BE), + // AesGcm(Todo?) + // AesCtr(Todo?) + NoCipher, +} + +impl EncKey { + /// Construct a key + pub fn from_cipher(cipher: &Cipher, key: &[u8], iv: &[u8]) -> Result<Self, Error> { + match cipher { + Cipher::ChaPoly => { + let key: &[u8; 64] = key.try_into().map_err(|_| Error::Bug)?; + Ok(EncKey::ChaPoly(chapoly::SealingKey::new(key))) + } + Cipher::Aes256Ctr => { + let key: &[u8; 32] = key.try_into().map_err(|_| Error::Bug)?; + let iv: &[u8; 16] = iv.try_into().map_err(|_| Error::Bug)?; + Ok(EncKey::Aes256Ctr(Aes256Ctr32BE::new(key.into(), iv.into()))) + } + } + } + pub fn is_aead(&self) -> bool { + match self { + EncKey::ChaPoly(_) => true, + EncKey::Aes256Ctr(_a) => false, + EncKey::NoCipher => false, + } + } + pub fn size_block(&self) -> usize { + match self { + EncKey::ChaPoly(_) => SSH_MIN_BLOCK, + EncKey::Aes256Ctr(_) => aes::Aes256::block_size(), + EncKey::NoCipher => SSH_MIN_BLOCK, + } + } +} + +pub(crate) enum DecKey { + ChaPoly(chapoly::OpeningKey), + Aes256Ctr(Aes256Ctr32BE), + // AesGcm256 + // AesCtr256 + NoCipher, +} + +impl DecKey { + /// Construct a key + pub fn from_cipher(cipher: &Cipher, key: &[u8], iv: &[u8]) -> Result<Self, Error> { + match cipher { + Cipher::ChaPoly => { + let key: &[u8; 64] = key.try_into().map_err(|_| Error::Bug)?; + Ok(DecKey::ChaPoly(chapoly::OpeningKey::new(key))) + } + Cipher::Aes256Ctr => { + let key: &[u8; 32] = key.try_into().map_err(|_| Error::Bug)?; + let iv: &[u8; 16] = iv.try_into().map_err(|_| Error::Bug)?; + Ok(DecKey::Aes256Ctr(Aes256Ctr32BE::new(key.into(), iv.into()))) + } + } + } + pub fn is_aead(&self) -> bool { + match self { + DecKey::ChaPoly(_) => true, + DecKey::Aes256Ctr(_a) => false, + DecKey::NoCipher => false, + } + } + pub fn size_block(&self) -> usize { + match self { + DecKey::ChaPoly(_) => SSH_MIN_BLOCK, + DecKey::Aes256Ctr(_) => aes::Aes256::block_size(), + DecKey::NoCipher => SSH_MIN_BLOCK, + } + } +} + +pub(crate) enum Integ { + ChaPoly, + HmacSha256, + // aesgcm? +} + +/// Placeholder for a [`IntegKey`] type prior to keying. For use during key setup in [`kex`] +impl Integ { + /// Matches a MAC name. Should not be called for AEAD ciphers, instead use [`EncKey::integ`] etc + pub fn from_name(name: &str) -> Result<Self, Error> { + // TODO: match standalone HMAC names here. + match name { + SSH_NAME_HMAC_SHA256 => Ok(Integ::HmacSha256), + _ => Err(Error::Bug), + } + } +} + +pub(crate) enum IntegKey { + ChaPoly, + HmacSha256([u8; 32]), + // aesgcm? + // Sha2Hmac ? + NoInteg, +} + +impl IntegKey { + pub fn from_integ(integ: Integ, key: &[u8]) -> Self { + match integ { + Integ::ChaPoly => IntegKey::ChaPoly, + Integ::HmacSha256 => { + // hmac new_from_slice() can't fail. + let h = HmacSha256::new_from_slice(key).unwrap(); + IntegKey::HmacSha256(key.try_into().unwrap()) + } + } + } + pub fn size_out(&self) -> usize { + match self { + IntegKey::ChaPoly => chapoly::TAG_LEN, + IntegKey::HmacSha256(_) => sha2::Sha256::output_size(), + IntegKey::NoInteg => 0, + } + } +} +#[cfg(test)] +mod tests { + use crate::encrypt::{Keys, SSH_LENGTH_SIZE}; + use crate::error::Error; + #[allow(unused_imports)] + use log::{debug, error, info, log, trace, warn}; + + #[test] + fn roundtrip_nocipher() { + // check padding works + let keys = Keys::new_cleartext(); + for i in 0usize..40 { + let mut v: std::vec::Vec<u8> = (0u8..i as u8 + 30).collect(); + let orig_payload = v[SSH_LENGTH_SIZE..SSH_LENGTH_SIZE + i].to_vec(); + let seq = 123u32.rotate_left(i as u32); // something arbitrary + + let written = keys.encrypt(i, v.as_mut_slice(), seq).unwrap(); + + v.truncate(written); + let l = + keys.decrypt_first_block(v.as_mut_slice(), seq).unwrap() as usize; + keys.decrypt(v.as_mut_slice(), seq).unwrap(); + let dec_payload = v[SSH_LENGTH_SIZE..SSH_LENGTH_SIZE + i].to_vec(); + assert_eq!(written, l + SSH_LENGTH_SIZE + keys.integ_enc.size_out()); + assert_eq!(orig_payload, dec_payload); + } + } +} diff --git a/door-sshproto/src/error.rs b/door-sshproto/src/error.rs index 1e30c4ad670726057968dd17a31553e9895241ab..624c9c704e2e7e62a9b807982bf148a96826e4ec 100644 --- a/door-sshproto/src/error.rs +++ b/door-sshproto/src/error.rs @@ -1,22 +1,88 @@ -use snafu::{prelude::*}; +use core::str::Utf8Error; -#[derive(Snafu,Debug)] +use snafu::prelude::*; + +// TODO: can we make Snafu not require Debug? +#[non_exhaustive] +#[derive(Snafu, Debug)] pub enum Error { - /// Bad serialize - BadSerialize, - /// Buffer ran out of room - NoSpace, + /// Output buffer ran out of room + NoRoom, + + /// Input buffer ran out + RanOut, + /// Not implemented (unused in SSH protocol) NoSerializer, - /// Custom error - Custom, + + /// Not a UTF8 string + BadString, + + /// Decryption failure or integrity mismatch + BadDecrypt, + + /// Error in received SSH protocol + SSHProtoError, + + /// Unknown packet type + UnknownPacket, + + /// Received packet at a disallowed time + PacketWrong, + + /// No matching algorithm + AlgoNoMatch { algo: &'static str }, + + /// Packet size too large (or bad decrypt) + BigPacket, + + /// Random number generation failure + RngError, + + /// Other custom error + Custom { msg: &'static str }, + + /// Program bug. + /// This state should not be reached, previous logic should have prevented it. + Bug, } +impl Error { + pub fn msg(m: &'static str) -> Error { + Error::Custom { msg: m } + } +} + +impl From<Utf8Error> for Error { + fn from(e: Utf8Error) -> Error { + Error::BadString + } +} + +impl serde::de::StdError for Error {} + +// TODO: need to figure how to return our own Error variants from serde +// rather than using serde Error::custom(). impl serde::ser::Error for Error { fn custom<T>(msg: T) -> Self - where T:std::fmt::Display { - // TODO: something noalloc - println!("custom error: {}", msg.to_string()); - Error::Custom + where + T: core::fmt::Display, + { + #[cfg(feature = "std")] + println!("custom ser error: {}", msg); + + Error::msg("ser error") + } +} + +impl serde::de::Error for Error { + fn custom<T>(msg: T) -> Self + where + T: core::fmt::Display, + { + #[cfg(feature = "std")] + println!("custom de error: {}", msg); + + Error::msg("de error") } } diff --git a/door-sshproto/src/ident.rs b/door-sshproto/src/ident.rs new file mode 100644 index 0000000000000000000000000000000000000000..0ccd21a611b7f0c23b8a690c57b1534870223a14 --- /dev/null +++ b/door-sshproto/src/ident.rs @@ -0,0 +1,190 @@ +use crate::error::Error; + +pub(crate) const OUR_VERSION: &[u8] = "SSH-2.0-door".as_bytes(); + +const SSH_PREFIX: &[u8] = "SSH-2.0-".as_bytes(); + +// RFC4253 4.2 says max length 255 incl CR LF. +// TODO find what's in the wild +const MAX_REMOTE_VERSION_LEN: usize = 253; + +const CR: u8 = 0x0d; +const LF: u8 = 0x0a; + +/// Parses and stores the remove SSH version string +pub struct RemoteVersion { + storage: [u8; MAX_REMOTE_VERSION_LEN], + /// Parse state + st: VersPars, +} + +/// Version parsing state. +/// We need to match +/// `SSH-2.0-softwareversion SP comments CR LF` +/// at the start of a line. The server may first send other lines +/// which are discarded. +// TODO: SSH impls advertising SSH1 compatibility will have "SSH-1.99-" instead. +// We may need to handle parsing that as well for compatibility. It's possible +// they aren't common or important these days. + +#[derive(Debug)] +pub(crate) enum VersPars { + /// Reading start of a line, before receiving a full SSH-2.0- prefix + Start(usize), + /// Have a line that didn't start with SSH-2.0-, discarding until LF + Discarding, + /// Currently reading a SSH-2.0- string, waiting for ending CR + FillSSH(usize), + /// Have ending CR after a version, Waiting for ending LF + HaveCR(usize), + /// Completed string. + Done(usize), +} + +impl<'a> RemoteVersion { + pub fn new() -> Self { + RemoteVersion { + storage: [0; MAX_REMOTE_VERSION_LEN], + st: VersPars::Start(0), + } + } + + /// Returns the parsed version if stored. + pub fn version(&'a self) -> Option<&'a [u8]> { + match &self.st { + VersPars::Done(len) => { + let (s, _) = self.storage.split_at(*len); + Some(s) + } + _ => None, + } + } + + /// Reads the initial SSH stream to find the version string and returns + /// the number of bytes consumed, with bool set if complete. + /// Behaviour is undefined if called later after an error. + pub fn consume(&mut self, buf: &[u8]) -> Result<(usize, bool), Error> { + // consume input byte by byte, feeding through the states + let mut taken = 0; + for &b in buf { + match self.st { + VersPars::Done(_) => {} + _ => taken += 1, + } + + match self.st { + VersPars::Start(ref mut pos) => { + let w = self.storage.get_mut(*pos).ok_or(Error::NoRoom)?; + *w = b; + *pos += 1; + // Check if line so far matches SSH-2.0- + let (s, _) = self.storage.split_at(*pos); + if s == SSH_PREFIX { + self.st = VersPars::FillSSH(*pos) + } else if *pos <= SSH_PREFIX.len() { + let (ssh, _) = SSH_PREFIX.split_at(*pos); + if ssh != s { + self.st = VersPars::Discarding + } + } else { + self.st = VersPars::Discarding + } + } + + VersPars::Discarding => { + if b == LF { + self.st = VersPars::Start(0); + } + } + + VersPars::FillSSH(ref mut pos) => match b { + CR => { + let (s, _) = self.storage.split_at(*pos); + if !s.is_ascii() { + return Err(Error::msg("bad remote version")); + } + self.st = VersPars::HaveCR(*pos); + } + LF => { + return Err(Error::msg("bad remote version")); + } + _ => { + let w = self.storage.get_mut(*pos).ok_or(Error::NoRoom)?; + *w = b; + *pos += 1; + } + }, + VersPars::HaveCR(len) => { + match b { + LF => self.st = VersPars::Done(len), + _ => return Err(Error::msg("bad remote version")), + }; + } + + VersPars::Done(_) => { + break; + } + } + } + // Ran out of input + let done = matches!(self.st, VersPars::Done(_)); + Ok((taken, done)) + } +} + +#[cfg(test)] +#[rustfmt::skip] +mod tests { + use crate::ident; + use crate::error::Error; + + fn test_version(v: &str, split: usize, expect: &str) -> Result<usize, Error> { + let mut r = ident::RemoteVersion::new(); + + let split = split.min(v.len()); + let (a, b) = v.as_bytes().split_at(split); + + let (taken1, done1) = r.consume(a)?; + let (taken2, done2) = r.consume(b)?; + + if done1 { + assert!(done2); + assert!(taken2 == 0); + } + if taken2 > 0 { + assert_eq!(taken1, a.len()); + } + + let v = core::str::from_utf8(r.version().ok_or(Error::Bug)?)?; + assert_eq!(v, expect); + Ok(taken1 + taken2) + } + + #[test] + /// check round trip of packet enums is right + fn version() -> Result<(), Error> { + let long = core::str::from_utf8(&[60u8; 300]).unwrap(); + // split input at various positions + let splits = [ + (0..40).collect(), + vec![200,252,253,254,255,256], + ].concat(); + for &i in splits.iter() { + test_version("SSH-2.0-@\x0d\x0a", i, "SSH-2.0-@").unwrap(); + test_version("SSH-2.0-good something SSH-2.0-trick\x0d\x0azzz", i, "SSH-2.0-good something SSH-2.0-trick").unwrap(); + test_version("SSH-2.0-@\x0a\x0d", i, "").unwrap_err(); + test_version("SSH-2.0-@\x0a\x0d", i, "").unwrap_err(); + test_version("bleh \x0d\x0aSSH-2.0-@\x0d\x0a", i, "SSH-2.0-@").unwrap(); + assert_eq!(test_version("SSH-2.0-@\x0d\x0amore", i, "SSH-2.0-@").unwrap(), 11); + assert_eq!(test_version("\x0d\x0aSSH-2.0-@\x0d\x0amore", i, "SSH-2.0-@").unwrap(), 13); + test_version("\x0d\x0aSSH-2.0bleh \x0d\x0aSSH-2.0-@\x0d\x0a", i, "SSH-2.0-@").unwrap(); + + test_version(&long, i, "").unwrap_err(); + test_version(&format!("{long}\x0d\x0aSSH-2.0-works\x0d\x0a"), i, "SSH-2.0-works").unwrap(); + test_version(&format!("{long} \x0aSSH-2.0-works\x0d\x0a"), i, "SSH-2.0-works").unwrap(); + // a CR by itself is insufficient + test_version(&format!("{long} \x0dSSH-2.0-works\x0d\x0a"), i, "").unwrap_err(); + } + Ok(()) + } +} diff --git a/door-sshproto/src/kex.rs b/door-sshproto/src/kex.rs new file mode 100644 index 0000000000000000000000000000000000000000..ecf663c3a45ca10dfe57d7c45e9281ae43b6d4a1 --- /dev/null +++ b/door-sshproto/src/kex.rs @@ -0,0 +1,306 @@ +use crate::encrypt::{Cipher, Integ}; +use crate::ident::RemoteVersion; +use crate::namelist::LocalNames; +use crate::*; +use ring::digest::{self, Context as DigestCtx, Digest}; +#[allow(unused_imports)] +use { + crate::error::Error, + log::{debug, error, info, log, trace, warn}, +}; + +// RFC8731 +pub const SSH_NAME_CURVE25519: &str = "curve25519-sha256"; +// An older alias prior to standardisation. Eventually could be removed +pub const SSH_NAME_CURVE25519_LIBSSH: &str = "curve25519-sha256@libssh.org"; +// RFC8308 Extension Negotiation +pub const SSH_NAME_EXT_INFO_S: &str = "ext-info-s"; +pub const SSH_NAME_EXT_INFO_C: &str = "ext-info-c"; + +// RFC8709 +pub const SSH_NAME_ED25519: &str = "curve25519-sha256"; +// RFC8332 +pub const SSH_NAME_RSA_SHA256: &str = "rsa-sha2-256"; +// RFC4253 +pub const SSH_NAME_RSA_SHA1: &str = "ssh-rsa"; + +// RFC4344 +pub const SSH_NAME_AES256_CTR: &str = "aes256-ctr"; +// OpenSSH PROTOCOL.chacha20poly1305.txt +pub const SSH_NAME_CHAPOLY: &str = "chacha20-poly1305@openssh.com"; +// OpenSSH PROTOCOL. +pub const SSH_NAME_AES256_GCM: &str = "aes256-gcm@openssh.com"; +// (No-one uses aes-gcm RFC5647 from the NSA, it fails to define mac negotiation +// sensibly and has horrible naming style) + +// RFC6668 +pub const SSH_NAME_HMAC_SHA256: &str = "hmac-sha2-256"; + +// RFC4253 +pub const SSH_NAME_NONE: &str = "none"; + +const empty_localnames: LocalNames = LocalNames(&[]); + +// TODO this will be configurable. +const fixed_options_kex: LocalNames = + LocalNames(&[SSH_NAME_CURVE25519, SSH_NAME_CURVE25519_LIBSSH]); +const fixed_options_hostkey: LocalNames = + LocalNames(&[SSH_NAME_ED25519, SSH_NAME_RSA_SHA256, SSH_NAME_RSA_SHA1]); + +const fixed_options_cipher: LocalNames = LocalNames(&[SSH_NAME_CHAPOLY, SSH_NAME_AES256_CTR]); +const fixed_options_mac: LocalNames = LocalNames(&[SSH_NAME_HMAC_SHA256]); +const fixed_options_comp: LocalNames = LocalNames(&[SSH_NAME_NONE]); + +pub(crate) struct AlgoConfig<'a> { + kexs: LocalNames<'a>, + hostkeys: LocalNames<'a>, + ciphers: LocalNames<'a>, + macs: LocalNames<'a>, + comps: LocalNames<'a>, +} + +impl<'a> AlgoConfig<'a> { + /// Creates the standard algorithm configuration + /// TODO: ext-info-s and ext-info-c + pub fn new(is_client: bool) -> Self { + AlgoConfig { + kexs: fixed_options_kex, + hostkeys: fixed_options_hostkey, + ciphers: fixed_options_cipher, + macs: fixed_options_mac, + comps: fixed_options_comp, + } + } +} + +#[allow(non_snake_case)] +pub(crate) struct Kex { + // TODO: we could be tricky here and have an enum that saves memory + // by only keeping currently required fields. to be done once the structure + // stabilises + + // Cookie sent in our KexInit packet. Kept so that we can reproduce the + // KexInit packet when calculating the exchange hash. + our_cookie: [u8; 16], + + // populated once we have sent and received KexInit + algos: Option<Algos>, + // kexhash state. progessively include version idents, kexinit payloads, hostkey, e/f, secret + hash_ctx: Option<DigestCtx>, + + // populated after kex reply, from hash_ctx + H: Option<Digest>, +} + +enum KexState { + New, + SentKexInit, + RecvKexInit, + //... todo +} + +/// Records the chosen algorithms while key exchange proceeds +struct Algos { + kex: SharedSecret, + // hostkey: HostKey, + cipher_enc: Cipher, + cipher_dec: Cipher, + integ_enc: Integ, + integ_dec: Integ, +} + +impl Kex { + pub fn new() -> Self { + let mut our_cookie = [0u8; 16]; + random::fill_random(our_cookie.as_mut_slice()); + Kex { our_cookie, algos: None, hash_ctx: None, H: None } + } + pub fn handle_kexinit( + &mut self, is_client: bool, algo_conf: &AlgoConfig, + remote_version: &RemoteVersion, remote_kexinit: &packets::KexInit, + ) -> Result<(), Error> { + let algos = Self::algo_negotiation(is_client, remote_kexinit, algo_conf)?; + self.hash_ctx = Some(self.start_kexhash( + &algos, + is_client, + algo_conf, + remote_version, + remote_kexinit, + )?); + self.algos = Some(algos); + Ok(()) + } + + pub fn make_kexinit<'a>(&self, conf: &'a AlgoConfig) -> packets::KexInit<'a> { + packets::KexInit { + cookie: self.our_cookie, + kex: (&conf.kexs).into(), + hostkey: (&conf.hostkeys).into(), + cipher_c2s: (&conf.ciphers).into(), + cipher_s2c: (&conf.ciphers).into(), + mac_c2s: (&conf.macs).into(), + mac_s2c: (&conf.macs).into(), + comp_c2s: (&conf.comps).into(), + comp_s2c: (&conf.comps).into(), + lang_c2s: (&empty_localnames).into(), + lang_s2c: (&empty_localnames).into(), + first_follows: false, + reserved: 0, + } + } + + fn start_kexhash( + &mut self, algos: &Algos, is_client: bool, algo_conf: &AlgoConfig, remote_version: &RemoteVersion, + remote_kexinit: &packets::KexInit, + ) -> Result<DigestCtx, Error> { + // RFC4253 section 8: + // The hash H is computed as the HASH hash of the concatenation of the + // following: + // string V_C, the client's identification string (CR and LF + // excluded) + // string V_S, the server's identification string (CR and LF + // excluded) + // string I_C, the payload of the client's SSH_MSG_KEXINIT + // string I_S, the payload of the server's SSH_MSG_KEXINIT + // string K_S, the host key + // mpint e, exchange value sent by the client + // mpint f, exchange value sent by the server + // mpint K, the shared secret + + let mut hash_ctx = DigestCtx::new(algos.kex.get_hash()); + let remote_version = remote_version.version().ok_or(Error::Bug)?; + // Recreate our own kexinit packet to hash + let own_kexinit = self.make_kexinit(algo_conf); + if is_client { + hash_ctx.update(ident::OUR_VERSION); + hash_ctx.update(remote_version); + wireformat::hash_ssh(&mut hash_ctx, &own_kexinit)?; + wireformat::hash_ssh(&mut hash_ctx, remote_kexinit)?; + } else { + hash_ctx.update(remote_version); + hash_ctx.update(ident::OUR_VERSION); + wireformat::hash_ssh(&mut hash_ctx, remote_kexinit)?; + wireformat::hash_ssh(&mut hash_ctx, &own_kexinit)? + } + // The remainder of hash_ctx is updated after kexdhreply + + Ok(hash_ctx) + } + + /// Perform SSH algorithm negotiation + fn algo_negotiation( + is_client: bool, p: &packets::KexInit, conf: &AlgoConfig, + ) -> Result<Algos, Error> { + // For each algorithm we select the first name in the client's + // list that is also present in the server's list. + let kex_method = p + .kex + .first_protocol_match(is_client, &conf.kexs)? + .ok_or(Error::AlgoNoMatch { algo: "kex" })?; + let kex = SharedSecret::from_name(kex_method)?; + let hostkey_method = p + .hostkey + .first_protocol_match(is_client, &conf.hostkeys)? + .ok_or(Error::AlgoNoMatch { algo: "hostkey" })?; + + // Switch between client/server tx/rx + let c2s = (&p.cipher_c2s, &p.mac_c2s, &p.comp_c2s); + let s2c = (&p.cipher_s2c, &p.mac_s2c, &p.comp_s2c); + let ((cipher_tx, mac_tx, comp_tx), (cipher_rx, mac_rx, comp_rx)) = + if is_client { (c2s, s2c) } else { (s2c, c2s) }; + + let n = cipher_tx + .first_protocol_match(is_client, &conf.ciphers)? + .ok_or(Error::AlgoNoMatch { algo: "encryption" })?; + let cipher_enc = Cipher::from_name(n)?; + let n = cipher_rx + .first_protocol_match(is_client, &conf.ciphers)? + .ok_or(Error::AlgoNoMatch { algo: "encryption" })?; + let cipher_dec = Cipher::from_name(n)?; + + // We ignore mac algorithms for AEAD ciphers + let integ_enc = if let Some(integ) = cipher_enc.integ() { + integ + } else { + let n = mac_tx + .first_protocol_match(is_client, &conf.macs)? + .ok_or(Error::AlgoNoMatch { algo: "mac" })?; + Integ::from_name(n)? + }; + let integ_dec = if let Some(integ) = cipher_dec.integ() { + integ + } else { + let n = mac_rx + .first_protocol_match(is_client, &conf.macs)? + .ok_or(Error::AlgoNoMatch { algo: "mac" })?; + Integ::from_name(n)? + }; + + // Compression only matches "none", we don't need further handling + // at the moment. + comp_tx + .first_protocol_match(is_client, &conf.comps)? + .ok_or(Error::AlgoNoMatch { algo: "compression" })?; + comp_rx + .first_protocol_match(is_client, &conf.comps)? + .ok_or(Error::AlgoNoMatch { algo: "compression" })?; + + // Ignore language fields at present. unsure which implementations + // use it, possibly SunSSH + + Ok(Algos { kex, cipher_enc, cipher_dec, integ_enc, integ_dec }) + } +} + +enum SharedSecret { + KexCurve25519(KexCurve25519), + // ECDH? +} + +impl SharedSecret { + pub fn from_name(name: &str) -> Result<Self, Error> { + match name { + SSH_NAME_CURVE25519 | SSH_NAME_CURVE25519_LIBSSH => { + Ok(SharedSecret::KexCurve25519(KexCurve25519::new())) + } + _ => Err(Error::Bug), + } + } + pub fn get_hash(&self) -> &'static digest::Algorithm { + match self { + SharedSecret::KexCurve25519(_) => &digest::SHA256, + } + } +} + +struct KexCurve25519 {} + +impl KexCurve25519 { + fn new() -> Self { + KexCurve25519 {} + } +} + +#[cfg(test)] +mod tests { + use crate::encrypt; + use crate::error::Error; + use crate::kex; + + #[test] + fn test_name_match() { + // check that the from_name() functions are complete + for k in kex::fixed_options_kex.0.iter() { + println!("{k}"); + kex::SharedSecret::from_name(k).unwrap(); + } + for k in kex::fixed_options_cipher.0.iter() { + println!("{k}"); + encrypt::Cipher::from_name(k).unwrap(); + } + for k in kex::fixed_options_mac.0.iter() { + println!("{k}"); + encrypt::Integ::from_name(k).unwrap(); + } + } +} diff --git a/door-sshproto/src/lib.rs b/door-sshproto/src/lib.rs index 056bb2ef0bb78c08c37f827e2c1ad2847ba216b4..797cc458133d901bf5060e8f3080d29dc4a8f5a4 100644 --- a/door-sshproto/src/lib.rs +++ b/door-sshproto/src/lib.rs @@ -1,15 +1,21 @@ +// #![no_std] #![forbid(unsafe_code)] +// XXX unused_imports only during dev churn +#![allow(unused_imports)] + +#[cfg(test)] +#[macro_use] +extern crate std; pub mod packets; -// XXX public? -pub mod wireformat; +// XXX decide what is public +pub mod conn; +pub mod encrypt; pub mod error; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} +pub mod ident; +pub mod kex; +pub mod test; +pub mod traffic; +pub mod wireformat; +pub mod namelist; +pub mod random; diff --git a/door-sshproto/src/namelist.rs b/door-sshproto/src/namelist.rs new file mode 100644 index 0000000000000000000000000000000000000000..0f19c6c04e1e61a26040706b1b3eec913ab2c083 --- /dev/null +++ b/door-sshproto/src/namelist.rs @@ -0,0 +1,171 @@ +//! SSH comma separated algorithm lists. +#[allow(unused_imports)] +use { + crate::error::Error, + log::{debug, error, info, log, trace, warn}, +}; + +use serde::de; +use serde::de::{DeserializeSeed, SeqAccess, Visitor}; +use serde::ser::{SerializeSeq, SerializeTuple, Serializer}; +use serde::Deserializer; + +use serde::{Deserialize, Serialize}; + + +/// A comma separated string, can be deserialized or serialized. +/// Used for remote name lists. +#[derive(Serialize, Deserialize, Debug)] +pub struct StringNames<'a>(pub &'a str); + +/// A list of names, can only be serialized. Used for local name lists, comes +/// from local fixed lists +/// Deliberately 'static since it should only come from hardcoded local strings +/// SSH_NAME_* in [`kex`]. We don't validate string contents. +#[derive(Debug)] +pub struct LocalNames<'a>(pub &'a[&'static str]); + +/// The general form that can store either representation +#[derive(Serialize, Debug)] +pub enum NameList<'a> { + String(StringNames<'a>), + Local(LocalNames<'a>), +} + +impl<'de: 'a, 'a> Deserialize<'de> for NameList<'a> { + fn deserialize<D>(deserializer: D) -> Result<NameList<'a>, D::Error> + where + D: Deserializer<'de>, + { + let s = StringNames::deserialize(deserializer)?; + if s.0.is_ascii() { + Ok(NameList::String(s)) + } else { + Err(de::Error::custom("algorithm isn't ascii")) + } + } +} + +/// Serialize the list of names with comma separators +impl<'a> Serialize for LocalNames<'a> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + let names = &self.0; + for i in 0..names.len() { + seq.serialize_element(names[i].as_bytes()); + if i < names.len()-1 { + seq.serialize_element(&(',' as u8)); + } + } + seq.end() + } +} + +impl<'a> From<&'a str> for StringNames<'a> { + fn from(s: &'a str) -> Self { + Self(s) + } +} +impl<'a> From<&'a [&'static str]> for LocalNames<'a> { + fn from(s: &'a [&'static str]) -> Self { + Self(s) + } +} +impl<'a> From<&'a str> for NameList<'a> { + fn from(s: &'a str) -> Self { + NameList::String(s.into()) + } +} +impl<'a> Into<NameList<'a>> for &LocalNames<'a> { + fn into(self) -> NameList<'a> { + NameList::Local(LocalNames(self.0)) + } + +} + +impl<'a> NameList<'a> { + /// Returns the first name in this namelist that matches, based on SSH priority. + /// The SSH client's list (which could be either remote or ours) is used + /// to determine priority. + /// `self` is a remote list, `our_options` are our own allowed options in preference + /// order. + /// Must only be called on [`StringNames`], will fail if called with self as [`LocalNames`]. + pub fn first_protocol_match( + &self, is_client: bool, our_options: &LocalNames, + ) -> Result<Option<&str>, Error> { + match self { + NameList::String(s) => { + Ok(if is_client { + s.first_match(our_options) + } else { + s.first_options_match(our_options) + }) + }, + NameList::Local(_) => Err(Error::Bug) + } + } +} + +impl<'a> StringNames<'a> { + /// Returns the first name in this namelist that matches one of the provided options + fn first_match(&self, options: &LocalNames) -> Option<&str> { + trace!("match {:?} options {:?}", self, options); + for n in self.0.split(',') { + for o in options.0.iter() { + trace!("match {} options {} {}", n, *o, (*n == **o)); + if n == *o { + return Some(n); + } + } + } + trace!("None"); + None + } + + /// Returns the first of "options" that is in this namelist + fn first_options_match(&self, options: &LocalNames) -> Option<&str> { + trace!("firstopmatch {:?} options {:?}", self, options); + for o in options.0.iter() { + for n in self.0.split(',') { + if n == *o { + return Some(n); + } + } + } + None + } +} + +#[cfg(test)] +mod tests { + use crate::{wireformat}; + use crate::namelist::*; + use pretty_hex::PrettyHex; + + + #[test] + fn test_localnames_serialize() { + let tests: Vec<&[&str]> = vec![ + &["foo", "quux", "boo"], + &[], + &["one"], + &["one", "2"], + &["", "2"], + &["3", ""], + &["", ""], + &[",", ","], // not really valid + ]; + for t in tests.iter() { + let n = NameList::Local(LocalNames(t)); + let mut buf = vec![99; 30]; + let l = wireformat::write_ssh(&mut buf, &n).unwrap(); + buf.truncate(l); + let out1 = core::str::from_utf8(&buf).unwrap(); + // check that a join with std gives the same result. + assert_eq!(out1, t.join(",")); + } + } +} diff --git a/door-sshproto/src/packets.rs b/door-sshproto/src/packets.rs index 21674843bdf3a131e789e61f3b83271cfb89aa71..437e2b1093c265a4b04d84c634dfb0ee860c32a6 100644 --- a/door-sshproto/src/packets.rs +++ b/door-sshproto/src/packets.rs @@ -1,38 +1,186 @@ -use serde::{Serialize, Deserialize}; +//! SSH protocol packets. A [`Packet`] can be serialized/deserialized to the +//! SSH Binary Packet Protocol using [`serde`] with [`crate::wireformat`]. +//! +//! These are mostly container formats though there is some logic to determine +//! which enum variant needs deserializing for certain packet types. +//! +//! Some packet formats are self describing, eg [`UserauthRequest`] has a `method` +//! string that switches between [`MethodPubkey`] and [`MethodPassword`]. Other packets +//! such as [`KexDHReply`] don't have that structure, instead they depend on previous +//! state of the SSH session. That state is passed with [`ParseContext`]. +use core::borrow::BorrowMut; +use core::cell::Cell; +use core::fmt; +use core::marker::PhantomData; -#[derive(Serialize, Deserialize)] -pub struct Packet<'a> { - pub ty: u8, - #[serde(borrow)] - pub p: SpecificPacket<'a>, +use serde::de; +use serde::de::{DeserializeSeed, SeqAccess, Visitor}; +use serde::ser::{SerializeSeq, SerializeTuple, Serializer}; +use serde::Deserializer; + +use serde::{Deserialize, Serialize}; + +use crate::error::Error; +use crate::namelist::NameList; +use crate::wireformat::BinString; + +/// Negotiated Key Exchange (KEX) type, used to parse kexinit/kexreply packets. +#[derive(Debug)] +pub enum KexType { + Unset, + Curve25519, + DiffieHellman, } -#[derive(Serialize, Deserialize)] -pub enum SpecificPacket<'a> { - #[serde(borrow)] - KexInit(KexInit<'a>), - KexDHInit(KexDHInit), - KexDHReply(KexDHReply), +/// State to be passed to deserialisation. Use this so the parser can select the correct +/// enum variant to deserialize. +pub struct ParseContext { + pub kextype: KexType, } -// TODO: impl matching - // XXX - how does a str reference work? prob needs to be [u8]? -#[derive(Serialize, Deserialize, Debug)] -pub struct NameList<'a>(&'a str); +impl ParseContext { + pub fn new() -> Self { + ParseContext { kextype: KexType::Unset } + } +} + +/// State passed as the Deserializer seed. +pub(crate) struct PacketState<'a> { + pub ctx: &'a ParseContext, + // Private fields that keep state during parsing. + // TODO Perhaps not actually necessary, could be removed and just pass ParseContext? + // pub(crate) ty: Cell<Option<MessageNumber>>, +} + +#[derive(Debug)] +#[repr(u8)] +#[allow(non_camel_case_types)] +pub enum MessageNumber { + SSH_MSG_KEXINIT = 20, + SSH_MSG_KEXDH_INIT = 30, + SSH_MSG_KEXDH_REPLY = 31, + SSH_MSG_USERAUTH_REQUEST = 50, +} + +impl TryFrom<u8> for MessageNumber { + type Error = Error; + fn try_from(v: u8) -> Result<Self, Error> { + match v { + 20 => Ok(MessageNumber::SSH_MSG_KEXINIT), + 30 => Ok(MessageNumber::SSH_MSG_KEXDH_INIT), + 31 => Ok(MessageNumber::SSH_MSG_KEXDH_REPLY), + 50 => Ok(MessageNumber::SSH_MSG_USERAUTH_REQUEST), + _ => Err(Error::UnknownPacket), + } + } +} -impl<'a> From<&'a str> for NameList<'a> { - fn from(s: &'a str) -> Self { - Self(s) +/// Some packets require context to parse, so we pass PacketState +pub(crate) struct DeserPacket<'a>(pub(crate) &'a PacketState<'a>); + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DeserPacket<'a> { + type Value = Packet<'de>; + fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where + D: Deserializer<'de>, + { + struct PacketVisitor<'b> { + seed: &'b PacketState<'b>, + } + + impl<'de: 'b, 'b> Visitor<'de> for PacketVisitor<'b> { + type Value = Packet<'de>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct Packet") + } + fn visit_seq<V>(self, mut seq: V) -> Result<Packet<'de>, V::Error> + where + V: SeqAccess<'de>, + { + // First byte is always message number + let msg_num: u8 = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let ty: MessageNumber = msg_num + .try_into() + .map_err(|_| de::Error::custom("Unknown packet type"))?; + + // Decode based on the message number + let p = match ty { + MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit( + seq.next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?, + ), + MessageNumber::SSH_MSG_KEXDH_INIT => Packet::KexDHInit( + seq.next_element_seed(DeserKexDHInit(self.seed))? + .ok_or_else(|| de::Error::invalid_length(1, &self))?, + ), + MessageNumber::SSH_MSG_KEXDH_REPLY => Packet::KexDHReply( + seq.next_element_seed(DeserKexDHReply(self.seed))? + .ok_or_else(|| de::Error::invalid_length(1, &self))?, + ), + MessageNumber::SSH_MSG_USERAUTH_REQUEST => todo!("userauth"), + }; + + Ok(p) + } + } + deserializer.deserialize_seq(PacketVisitor { seed: self.0 }) + } +} + +impl<'a> Serialize for Packet<'a> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + + match self { + Packet::KexInit(p) => { + let t = MessageNumber::SSH_MSG_KEXINIT as u8; + seq.serialize_element(&t)?; + seq.serialize_element(p)?; + } + Packet::KexDHInit(p) => { + let t = MessageNumber::SSH_MSG_KEXDH_INIT as u8; + seq.serialize_element(&t)?; + seq.serialize_element(p)?; + } + Packet::KexDHReply(p) => { + let t = MessageNumber::SSH_MSG_KEXDH_REPLY as u8; + seq.serialize_element(&t)?; + seq.serialize_element(p)?; + } + Packet::UserauthRequest(p) => { + let t = MessageNumber::SSH_MSG_USERAUTH_REQUEST as u8; + seq.serialize_element(&t)?; + seq.serialize_element(p)?; + } + }; + + seq.end() } } +/// Top level SSH packet enum +#[derive(Debug)] +pub enum Packet<'a> { + KexInit(KexInit<'a>), + KexDHInit(KexDHInit<'a>), + KexDHReply(KexDHReply<'a>), + UserauthRequest(UserauthRequest<'a>), +} + #[derive(Serialize, Deserialize, Debug)] pub struct KexInit<'a> { - pub cookie: &'a [u8], // 16 bytes + pub cookie: [u8; 16], + #[serde(borrow)] pub kex: NameList<'a>, pub hostkey: NameList<'a>, - pub enc_c2s: NameList<'a>, - pub enc_s2c: NameList<'a>, + pub cipher_c2s: NameList<'a>, + pub cipher_s2c: NameList<'a>, pub mac_c2s: NameList<'a>, pub mac_s2c: NameList<'a>, pub comp_c2s: NameList<'a>, @@ -40,24 +188,135 @@ pub struct KexInit<'a> { pub lang_c2s: NameList<'a>, pub lang_s2c: NameList<'a>, pub first_follows: bool, - pub reserved: u32 + pub reserved: u32, +} + +#[derive(Serialize, Debug)] +pub enum KexDHInit<'a> { + Curve25519Init(Curve25519Init<'a>), + DiffieHellmanInit(DiffieHellmanInit), } -#[derive(Serialize, Deserialize)] -pub enum KexDHInit { - Curve25519Init(Curve25519Init), +/// Deserialize implementation for KexDHInit +struct DeserKexDHInit<'a>(&'a PacketState<'a>); + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DeserKexDHInit<'a> { + type Value = KexDHInit<'de>; + fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where + D: Deserializer<'de>, + { + // Use the algo variant that was negotiated in KEX + match self.0.ctx.kextype { + KexType::Curve25519 => Ok(KexDHInit::Curve25519Init( + Curve25519Init::deserialize(deserializer)?, + )), + KexType::DiffieHellman => Ok(KexDHInit::DiffieHellmanInit( + DiffieHellmanInit::deserialize(deserializer)?, + )), + KexType::Unset => Err(de::Error::custom("kextype not set")), + } + } +} + +#[derive(Serialize, Debug)] +pub enum KexDHReply<'a> { + Curve25519Reply(Curve25519Reply<'a>), + DiffieHellmanReply(DiffieHellmanReply<'a>), +} + +/// Deserialize implementation for KexDHReply +struct DeserKexDHReply<'a>(&'a PacketState<'a>); + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DeserKexDHReply<'a> { + type Value = KexDHReply<'de>; + fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where + D: Deserializer<'de>, + { + // Use the algo variant that was negotiated in KEX + match self.0.ctx.kextype { + KexType::Curve25519 => Ok(KexDHReply::Curve25519Reply( + Curve25519Reply::deserialize(deserializer)?, + )), + KexType::DiffieHellman => Ok(KexDHReply::DiffieHellmanReply( + DiffieHellmanReply::deserialize(deserializer)?, + )), + KexType::Unset => Err(de::Error::custom("kextype not set")), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Curve25519Init<'a> { + #[serde(borrow)] + pub q_c: BinString<'a>, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct Curve25519Reply<'a> { + #[serde(borrow)] + pub k_s: BinString<'a>, + pub q_s: BinString<'a>, + pub sig: BinString<'a>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DiffieHellmanInit { + pub e: u32, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct DiffieHellmanReply<'a> { + #[serde(borrow)] + pub k_s: BinString<'a>, + pub f: BinString<'a>, // mpint + pub sig: BinString<'a>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct UserauthRequest<'a> { + pub username: &'a str, + pub service: &'a str, + pub method: &'a str, + // TODO: need to deserialize AuthMethod enum + pub a: AuthMethod<'a>, } -#[derive(Serialize, Deserialize)] -pub enum KexDHReply { - Curve25519Reply(Curve25519Reply), +/// The method-specific part of a [`UserauthRequest`]. +#[derive(Serialize, Deserialize, Debug)] +pub enum AuthMethod<'a> { + #[serde(borrow)] + Password(MethodPassword<'a>), + Pubkey(MethodPubkey<'a>), } -#[derive(Serialize, Deserialize)] -pub struct Curve25519Init { +#[derive(Serialize, Deserialize, Debug)] +pub struct MethodPassword<'a> { + pub change: bool, + pub password: &'a str, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct MethodPubkey<'a> { + pub trial: bool, + pub algo: &'a str, + pub pubkey: &'a [u8], + // TODO: need to deserialize sig as an Option + pub sig: Option<&'a [u8]>, } -#[derive(Serialize, Deserialize)] -pub struct Curve25519Reply { + +#[cfg(test)] +mod tests { + use crate::{packets,wireformat}; + + #[test] + /// check round trip of packet enums is right + fn packet_type() { + for i in 0..=255 { + let ty = packets::MessageNumber::try_from(i); + if let Ok(ty) = ty { + assert_eq!(i, ty as u8); + } + } + } } diff --git a/door-sshproto/src/random.rs b/door-sshproto/src/random.rs new file mode 100644 index 0000000000000000000000000000000000000000..202581a3c7a2584da10eaa490a023ea9c2335670 --- /dev/null +++ b/door-sshproto/src/random.rs @@ -0,0 +1,8 @@ +use rand::RngCore; +use crate::error::Error; + +pub fn fill_random(buf: &mut [u8]) -> Result<(), Error> { + // TODO: can this return an error? + rand::rngs::OsRng.fill_bytes(buf); + Ok(()) +} diff --git a/door-sshproto/src/test.rs b/door-sshproto/src/test.rs new file mode 100644 index 0000000000000000000000000000000000000000..b0b77cad19a437ef1b454732f7f4b6210b0e4884 --- /dev/null +++ b/door-sshproto/src/test.rs @@ -0,0 +1,67 @@ +#[cfg(test)] +mod tests { + use crate::error::Error; + use crate::packets::*; + use crate::wireformat::BinString; + use crate::packets::{KexType, Packet}; + use crate::{packets, wireformat}; + use pretty_hex::PrettyHex; + use serde::de::Unexpected; + use serde::{Deserialize, Serialize}; + + fn test_roundtrip_packet( + p: &Packet, kextype: packets::KexType, + ) -> Result<(), Error> { + let mut buf1 = vec![99; 500]; + let _w1 = wireformat::write_ssh(&mut buf1, &p)?; + + let mut ctx = packets::ParseContext::new(); + ctx.kextype = kextype; + + let p2 = wireformat::packet_from_bytes(&buf1, &ctx)?; + + let mut buf2 = vec![99; 500]; + let _w2 = wireformat::write_ssh(&mut buf2, &p2)?; + // println!("{p:?}"); + // println!("{p2:?}"); + // println!("{:?}", buf1.hex_dump()); + // println!("{:?}", buf2.hex_dump()); + + assert_eq!(buf1, buf2); + Ok(()) + } + + #[test] + fn roundtrip_kexinit() { + let k = KexInit { + cookie: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + kex: "kex".into(), + hostkey: "hostkey,another".into(), + cipher_c2s: "chacha20-poly1305@openssh.com,aes128-ctr".into(), + cipher_s2c: "blowfish".into(), + mac_c2s: "hmac-sha1".into(), + mac_s2c: "hmac-md5".into(), + comp_c2s: "none".into(), + comp_s2c: "".into(), + lang_c2s: "".into(), + lang_s2c: "".into(), + first_follows: true, + reserved: 0x6148291e, + }; + let p = Packet::KexInit(k); + test_roundtrip_packet(&p, KexType::Unset).unwrap(); + } + + #[test] + fn roundtrip_packet_kexdh() { + // XXX: this should break later if the q_c length is + let bs = BinString(&[0x11, 0x22, 0x33]); + let p = + Packet::KexDHInit(KexDHInit::Curve25519Init(Curve25519Init { q_c: bs })); + + test_roundtrip_packet(&p, KexType::Curve25519).unwrap(); + // packet format needs to be consistent + // test_roundtrip_packet(&p, KexType::DiffieHellman).unwrap_err(); + // test_roundtrip_packet(&p, KexType::Unset).unwrap_err(); + } +} diff --git a/door-sshproto/src/traffic.rs b/door-sshproto/src/traffic.rs new file mode 100644 index 0000000000000000000000000000000000000000..32c4cd5b454a56b69d9e482a82146e9b6cb1118f --- /dev/null +++ b/door-sshproto/src/traffic.rs @@ -0,0 +1,200 @@ +#[allow(unused_imports)] +use { + crate::error::Error, + log::{debug, error, info, log, trace, warn}, +}; + +use crate::encrypt::SSH_LENGTH_SIZE; +use crate::encrypt::KeyState; +use crate::ident::{RemoteVersion}; +use crate::*; + +pub(crate) struct Traffic<'a> { + // TODO: if smoltcp exposed both ends of a CircularBuffer to recv() + // we could perhaps just work directly in smoltcp's provided buffer? + // Would need changes to ring chapoly_openssh and block ciphers. + + // TODO: decompression will need another buffer + + /// Accumulated input or output buffer. + /// Should be sized to fit the largest packet allowed. + /// Contains ciphertext or cleartext, encrypted/decrypted in-place. + buf: &'a mut [u8], + state: TrafState, +} + +/// State machine for reads/writes sharing [`Traffic::buf`] +#[derive(Debug)] +enum TrafState { + /// buf is unused + Idle, + + /// Reading remote version, not SSH packet format + Version, + /// Reading initial block for packet length. idx > 0. + ReadInitial { + idx: usize, + }, + /// Reading remainder of encrypted packet + Read { + idx: usize, + expect: usize, + }, + /// Whole encryped packet has been read + ReadComplete { + len: usize, + }, + /// Decrypted complete input payload + InPayload { + len: usize, + }, + + OutPayload { + len: usize, + }, + /// Encrypted, writing to the socket + Write { + idx: usize, + len: usize, + }, +} + +impl<'a> Traffic<'a> { + pub fn new(buf: &'a mut [u8]) -> Self { + Traffic { + buf, + state: TrafState::Version, + } + } + + /// Returns the number of bytes consumed, and optionally + /// a complete packet payload. + pub fn input(&mut self, keys: &mut KeyState, remote_version: &mut RemoteVersion, + buf: &[u8]) -> Result<(usize, Option<&[u8]>), Error> { + let mut inlen = 0; + trace!("input() state: {:?}", self.state); + if let TrafState::Version = self.state { + // handle initial version string + let (l, done) = remote_version.consume(buf)?; + trace!("input() l: {l} {done}"); + if done { + self.state = TrafState::Idle + } + inlen += l; + } + let buf = &buf[inlen..]; + + inlen += self.fill_input(keys, buf)?; + + trace!("input() state2: {:?}", self.state); + let payload = if let TrafState::InPayload { len } = self.state { + use pretty_hex::PrettyHex; + let payload = &self.buf[SSH_LENGTH_SIZE + 1..SSH_LENGTH_SIZE + 1 + len]; + self.state = TrafState::Idle; + trace!("pyload {:?}", &payload.hex_dump()); + Some(payload) + } else { + None + }; + + + trace!("input() state2: {:?}", self.state); + + Ok((inlen, payload)) + } + + /// Write any pending output, returning the size written + pub fn output(&mut self, keys: &mut KeyState, buf: &mut [u8]) -> Result<usize, Error> { + if let TrafState::OutPayload { len } = self.state { + // Payload ready, encrypt it + let len = keys.encrypt(len, self.buf)?; + self.state = TrafState::Write { idx: 0, len }; + } + + match self.state { + TrafState::Write { ref mut idx, len } => { + let wlen = (len - *idx).min(buf.len()); + buf.copy_from_slice(&self.buf[*idx..*idx + wlen]); + *idx += wlen; + + if *idx == len { + // all done, read the next packet + self.state = TrafState::Idle + } + Ok(wlen) + } + _ => Ok(0), + } + } + + fn fill_input(&mut self, keys: &mut KeyState, buf: &[u8]) -> Result<usize, Error> { + let size_block = keys.size_block_dec(); + let size_integ = keys.size_integ_dec(); + // 'r' is the remaining input, a slice that moves along. + // Used to calculate the size to return + let mut r = buf; + + // Either Idle with input, or filling the initial block + if let Some(idx) = match self.state { + TrafState::Idle if r.len() > 0 => Some(0), + TrafState::ReadInitial { idx } => Some(idx), + _ => None, + } { + let need = (size_block - idx).clamp(0, r.len()); + let x; + (x, r) = r.split_at(need); + let w = &mut self.buf[idx..idx + need]; + w.copy_from_slice(x); + self.state = TrafState::ReadInitial { idx: idx + need } + } + + // Have enough input now to decrypt the packet length + if let TrafState::ReadInitial { idx } = self.state { + if idx >= size_block { + let w = &mut self.buf[..size_block]; + let total_len = + keys + .decrypt_first_block(w)? + .checked_add((SSH_LENGTH_SIZE + size_integ) as u32) + .ok_or(Error::BadDecrypt)? as usize; + + if total_len > self.buf.len() { + // TODO: Or just BadDecrypt could make more sense if + // it were packet corruption/decryption failure + warn!("total_len {total_len:08x}"); + return Err(Error::BigPacket); + } + self.state = TrafState::Read { idx, expect: total_len } + } + } + + // Know expected length, read until the end of the packet. + // We have already validated that expect_len <= buf_size + if let TrafState::Read { ref mut idx, expect } = self.state { + let need = (expect - *idx).min(r.len()); + let x; + (x, r) = r.split_at(need); + let w = &mut self.buf[*idx..*idx + need]; + w.copy_from_slice(x); + *idx += need; + if *idx == expect { + self.state = TrafState::ReadComplete { len: expect } + } + } + + if let TrafState::ReadComplete { len } = self.state { + let w = &mut self.buf[SSH_LENGTH_SIZE..len]; + keys.decrypt(w)?; + let padlen = w[0] as usize; + trace!("len {len} sub {} padlen {padlen}", + SSH_LENGTH_SIZE + 1 + size_integ + padlen); + let payload_len = len + .checked_sub(SSH_LENGTH_SIZE + 1 + size_integ + padlen) + .ok_or(Error::SSHProtoError)?; + + self.state = TrafState::InPayload { len: payload_len } + } + + Ok(buf.len() - r.len()) + } +} diff --git a/door-sshproto/src/wireformat.rs b/door-sshproto/src/wireformat.rs index c302d460f2a048b59578b4cd52e1b2d0357d012e..3a632317ffcc64729bea7fee18a17734b7f5ea39 100644 --- a/door-sshproto/src/wireformat.rs +++ b/door-sshproto/src/wireformat.rs @@ -1,56 +1,138 @@ -use serde::{ser, de, Serializer, Serialize, Deserializer, Deserialize}; +//! SSH protocol serialization. +//! Implements enough of serde to handle the formats defined in [`crate::packets`] + +//! See [RFC4251](https://datatracker.ietf.org/doc/html/rfc4251) for encodings, +//! [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253) and others for packet structure +use serde::{ + de, ser, + de::{DeserializeSeed, SeqAccess, Visitor}, + ser::SerializeSeq, + Deserialize, Deserializer, Serialize, Serializer, +}; + use crate::error::Error; -use core::result::{Result}; +use crate::packets::{DeserPacket, Packet, PacketState, ParseContext}; +use core::cell::Cell; +use core::result::Result; use core::slice; -/// See rfc4251 for encodings +/// Parses a [`Packet`] from a borrowed `&[u8]` byte buffer. +/// Requires [`ParseContext`] to indicate which Kex variant is being use. +pub fn packet_from_bytes<'a>( + b: &'a [u8], ctx: &ParseContext, +) -> Result<Packet<'a>, Error> { + let seed = PacketState { + ctx, + // ty: Cell::new(None), + }; -pub struct SeSSH<'a> { - target: &'a mut[u8], - pos: usize, + let mut ds = DeSSHBytes::from_bytes(b); + let t = DeserPacket(&seed).deserialize(&mut ds)?; + Ok(t) + // TODO check for trailing bytes, pos != b.len() } -type Res = Result<(), Error>; -/// Returns the length written. -// TODO: is there a nicer way? IterMut? +/// Writes a SSH packet to a buffer. Returns the length written. pub fn write_ssh<T>(target: &mut [u8], value: &T) -> Result<usize, Error> - where T: Serialize { - let mut serializer = SeSSH { - target, - pos: 0, - }; +where + T: Serialize, +{ + let mut serializer = SeSSHBytes::WriteBytes { target, pos: 0 }; value.serialize(&mut serializer)?; - Ok(serializer.pos) + Ok(match serializer { + SeSSHBytes::WriteBytes { target: _, pos } => pos, + _ => 0, // TODO is there a better syntax here? we know it's always WriteBytes + }) +} + +/// Hashes the contents of a SSH packet, updating the provided context. +pub fn hash_ssh<T>(hash_ctx: &mut ring::digest::Context, value: &T) -> Result<(), Error> +where + T: Serialize, +{ + let mut serializer = SeSSHBytes::WriteHash { hash_ctx }; + value.serialize(&mut serializer)?; + Ok(()) +} + +type Res = Result<(), Error>; + +#[derive(Deserialize, Debug)] +/// A SSH style binary string. 32 bit length followed by bytes. +pub struct BinString<'a>(pub &'a [u8]); + + +// Helper to pass a serde_state DeserializeState impl where a DeserializeSeed is needed, +// for next_element_seed() etc. +// struct SeedForState<'a, T, S> { +// seed: &'a mut S, +// value: PhantomData<T>, +// } + +// impl<'a, T, S> SeedForState<'a, T, S> { +// pub fn new(seed: &'a mut S) -> Self { +// Self { +// seed, +// value: PhantomData, +// } +// } +// } + +impl<'a> Serialize for BinString<'a> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + let l = self.0.len() as u32; + seq.serialize_element(&l)?; + seq.serialize_element(self.0)?; + seq.end() + } } -impl SeSSH<'_> { +/// Serializer for the SSH wire protocol. Writes into a borrowed `&mut [u8]` buffer. +/// Optionally compute the hash of the packet rather than serializing. +enum SeSSHBytes<'a> { + WriteBytes { target: &'a mut [u8], pos: usize }, + + WriteHash { hash_ctx: &'a mut ring::digest::Context } +} + +impl SeSSHBytes<'_> { fn push(&mut self, v: &[u8]) -> Res { - // TODO: can IterMut be used somehow? - if self.pos + v.len() > self.target.len() { - return Err(Error::NoSpace); + match self { + SeSSHBytes::WriteBytes { target, ref mut pos } => { + if *pos + v.len() > target.len() { + return Err(Error::NoRoom); + } + target[*pos..*pos + v.len()].copy_from_slice(v); + *pos += v.len(); + } + SeSSHBytes::WriteHash { hash_ctx } => { + hash_ctx.update(v); + } } - self.target[self.pos..self.pos + v.len()].copy_from_slice(v); - self.pos += v.len(); Ok(()) } } -impl Serializer for &mut SeSSH<'_> { +impl Serializer for &mut SeSSHBytes<'_> { type Ok = (); type Error = crate::error::Error; type SerializeSeq = Self; type SerializeStruct = Self; - type SerializeStructVariant = Self; - type SerializeTuple = ser::Impossible<(), Error>; + type SerializeTuple = Self; + type SerializeStructVariant = ser::Impossible<(), Error>; type SerializeTupleStruct = ser::Impossible<(), Error>; type SerializeTupleVariant = ser::Impossible<(), Error>; type SerializeMap = ser::Impossible<(), Error>; fn serialize_bool(self, v: bool) -> Res { - self.serialize_u32(v as u32) + self.serialize_u8(v as u8) } fn serialize_u8(self, v: u8) -> Res { self.push(&[v]) @@ -58,47 +140,65 @@ impl Serializer for &mut SeSSH<'_> { fn serialize_u32(self, v: u32) -> Res { self.push(&v.to_be_bytes()) } - // Not actually used in any SSH packets, mentioned in the arch doc + /// Not actually used in any SSH packets, mentioned in the arch doc fn serialize_u64(self, v: u64) -> Res { self.push(&v.to_be_bytes()) } - fn serialize_str(self, v: &str) -> Res { - self.serialize_u32(v.as_bytes().len() as u32)?; - self.push(v.as_bytes()) - } + /// Serialize raw bytes with no prefix fn serialize_bytes(self, v: &[u8]) -> Res { - self.push(v) + self.push(v)?; + todo!( + "This is asymmetric with deserialize_bytes, but isn't currently being used." + ) + } + fn serialize_str(self, v: &str) -> Res { + let b = v.as_bytes(); + self.serialize_u32(b.len() as u32)?; + self.push(b) } - // XXX Unsure if we're using Option fn serialize_none(self) -> Res { Ok(()) } fn serialize_some<T>(self, v: &T) -> Res - where T: ?Sized + Serialize { + where + T: ?Sized + Serialize, + { v.serialize(self) } fn serialize_newtype_struct<T>(self, _name: &'static str, v: &T) -> Res - where T: ?Sized + Serialize { + where + T: ?Sized + Serialize, + { v.serialize(self) } - fn serialize_newtype_variant<T>(self, - _name: &'static str, _variant_index: u32, _variant: &'static str, - v: &T) -> Res - where T: ?Sized + Serialize { + fn serialize_newtype_variant<T>( + self, _name: &'static str, _variant_index: u32, _variant: &'static str, + v: &T, + ) -> Res + where + T: ?Sized + Serialize, + { v.serialize(self) } - fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Error> { + fn serialize_seq( + self, _len: Option<usize>, + ) -> Result<Self::SerializeSeq, Error> { Ok(self) } - fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeSeq, Error> { + fn serialize_struct( + self, _name: &'static str, _len: usize, + ) -> Result<Self::SerializeSeq, Error> { Ok(self) } - fn serialize_struct_variant(self, - _name: &'static str, _variant_index: u32, _variant: &'static str, - _len: usize) -> Result<Self::SerializeSeq, Error> { + fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Error> { Ok(self) } + // Required for no_std + fn collect_str<T: ?Sized>(self, _: &T) -> Res { + Err(Error::NoSerializer) + } + // Not in the SSH protocol fn serialize_i8(self, _: i8) -> Res { Err(Error::NoSerializer) @@ -131,33 +231,42 @@ impl Serializer for &mut SeSSH<'_> { fn serialize_unit_struct(self, _name: &'static str) -> Res { Err(Error::NoSerializer) } - fn serialize_unit_variant(self, - _name: &'static str, _variant_index: u32, _variant: &'static str) -> Res { + fn serialize_unit_variant( + self, _name: &'static str, _variant_index: u32, _variant: &'static str, + ) -> Res { Err(Error::NoSerializer) } - fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Error> { + fn serialize_tuple_struct( + self, _name: &'static str, _len: usize, + ) -> Result<Self::SerializeTupleStruct, Error> { Err(Error::NoSerializer) } - fn serialize_tuple_struct(self, _name: &'static str, _len: usize) - -> Result<Self::SerializeTupleStruct, Error> { + fn serialize_tuple_variant( + self, _name: &'static str, _variant_index: u32, _variant: &'static str, + _len: usize, + ) -> Result<Self::SerializeTupleVariant, Error> { Err(Error::NoSerializer) } - fn serialize_tuple_variant(self, - _name: &'static str, _variant_index: u32, _variant: &'static str, - _len: usize) -> Result<Self::SerializeTupleVariant, Error> { + fn serialize_map( + self, _len: Option<usize>, + ) -> Result<Self::SerializeMap, Error> { Err(Error::NoSerializer) } - fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Error> { + fn serialize_struct_variant( + self, _name: &'static str, _variant_index: u32, _variant: &'static str, + _len: usize, + ) -> Result<Self::SerializeStructVariant, Error> { Err(Error::NoSerializer) } } -impl ser::SerializeSeq for &mut SeSSH<'_> { +impl ser::SerializeSeq for &mut SeSSHBytes<'_> { type Ok = (); type Error = crate::error::Error; fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error> - where T: ?Sized + Serialize, + where + T: ?Sized + Serialize, { value.serialize(&mut **self) } @@ -167,12 +276,15 @@ impl ser::SerializeSeq for &mut SeSSH<'_> { } } -impl ser::SerializeStruct for &mut SeSSH<'_> { +impl ser::SerializeStruct for &mut SeSSHBytes<'_> { type Ok = (); type Error = crate::error::Error; - fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error> - where T: ?Sized + Serialize, + fn serialize_field<T>( + &mut self, _key: &'static str, value: &T, + ) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, { value.serialize(&mut **self) } @@ -182,12 +294,13 @@ impl ser::SerializeStruct for &mut SeSSH<'_> { } } -impl ser::SerializeStructVariant for &mut SeSSH<'_> { +impl ser::SerializeTuple for &mut SeSSHBytes<'_> { type Ok = (); type Error = crate::error::Error; - fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error> - where T: ?Sized + Serialize, + fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, { value.serialize(&mut **self) } @@ -197,6 +310,228 @@ impl ser::SerializeStructVariant for &mut SeSSH<'_> { } } -pub struct DeSSH { +/// Deserializer for the SSH wire protocol, from borrowed `&[u8]` +/// Implements enough of serde to handle the formats defined in [`crate::packets`] +struct DeSSHBytes<'a> { + input: &'a [u8], + pos: usize, +} + +impl<'de> DeSSHBytes<'de> { + // XXX: rename to new() ? + pub fn from_bytes(input: &'de [u8]) -> Self { + DeSSHBytes { input, pos: 0 } + } + // #[inline] + fn take(&mut self, len: usize) -> Result<&'de [u8], Error> { + if len > self.input.len() { + return Err(Error::RanOut); + } + let (t, rest) = self.input.split_at(len); + self.input = rest; + self.pos += len; + Ok(t) + } + + #[inline] + fn parse_u8(&mut self) -> Result<u8, Error> { + let t = self.take(core::mem::size_of::<u8>())?; + let u = u8::from_be_bytes(t.try_into().unwrap()); + // println!("deser u8 {u}"); + Ok(u) + } + + #[inline] + fn parse_u32(&mut self) -> Result<u32, Error> { + let t = self.take(core::mem::size_of::<u32>())?; + let u = u32::from_be_bytes(t.try_into().unwrap()); + // println!("deser u32 {u}"); + Ok(u) + } + + fn parse_u64(&mut self) -> Result<u64, Error> { + let t = self.take(core::mem::size_of::<u64>())?; + Ok(u64::from_be_bytes(t.try_into().unwrap())) + } + + #[inline] + fn parse_str(&mut self) -> Result<&'de str, Error> { + let len = self.parse_u32()?; + let t = self.take(len as usize)?; + let s = core::str::from_utf8(t).map_err(|_| Error::BadString)?; + Ok(s) + } +} + +struct SeqAccessDeSSH<'a, 'b: 'a> { + ds: &'a mut DeSSHBytes<'b>, + len: Option<usize>, +} + +impl<'a, 'b: 'a> SeqAccess<'b> for SeqAccessDeSSH<'a, 'b> { + type Error = Error; + #[inline] + fn next_element_seed<V: DeserializeSeed<'b>>( + &mut self, seed: V, + ) -> Result<Option<V::Value>, Error> { + if let Some(ref mut len) = self.len { + if *len > 0 { + *len -= 1; + Ok(Some(DeserializeSeed::deserialize(seed, &mut *self.ds)?)) + } else { + Ok(None) + } + } else { + Ok(Some(DeserializeSeed::deserialize(seed, &mut *self.ds)?)) + } + } + + fn size_hint(&self) -> Option<usize> { + self.len + } +} + +impl<'de, 'a> Deserializer<'de> for &'a mut DeSSHBytes<'de> { + type Error = Error; + + fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + visitor.visit_bool(self.parse_u8()? != 0) + } + + fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + visitor.visit_u8(self.parse_u8()?) + } + + fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + visitor.visit_u32(self.parse_u32()?) + } + + fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + visitor.visit_u64(self.parse_u64()?) + } + + fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + visitor.visit_borrowed_str(self.parse_str()?) + } + + /* deserialize_bytes() is like a string but with binary data. it has + a u32 prefix of the length. Fixed length byte arrays use _tuple() */ + fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + let len = self.parse_u32()?; + let t = self.take(len as usize)?; + visitor.visit_borrowed_bytes(t) + } + + fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + visitor.visit_seq(SeqAccessDeSSH { ds: self, len: Some(len) }) + } + + fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error> + where + V: Visitor<'de>, + { + visitor.visit_seq(SeqAccessDeSSH { ds: self, len: None }) + } + + fn deserialize_struct<V>( + self, _name: &'static str, fields: &'static [&'static str], visitor: V, + ) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + self.deserialize_tuple(fields.len(), visitor) + } + + fn deserialize_enum<V>( + self, _name: &'static str, _variants: &'static [&'static str], _visitor: V, + ) -> Result<V::Value, Self::Error> + where + V: Visitor<'de>, + { + // visitor.visit_enum(self); + panic!("enum") + } + + fn deserialize_newtype_struct<V>( + self, _name: &'static str, visitor: V, + ) -> Result<V::Value, Self::Error> + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple_struct<V>( + self, _name: &'static str, _len: usize, _visitor: V, + ) -> Result<V::Value, Self::Error> + where + V: Visitor<'de>, + { + Err(Error::NoSerializer) + } + + fn deserialize_unit<V>(self, _visitor: V) -> Result<V::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!("unit") + } + // The remainder will fail. + serde::forward_to_deserialize_any! { + i8 i16 i32 i64 i128 u16 u128 f32 f64 char string + byte_buf unit_struct + map identifier ignored_any + option + } + fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Error> + where + V: Visitor<'de>, + { + Err(Error::NoSerializer) + } +} + +#[cfg(test)] +mod tests { + use crate::error::Error; + use crate::*; + // use pretty_hex::PrettyHex; + + #[test] + /// check that hash_ssh() matches hashing a serialized message + fn test_hash_ssh() { + use ring::digest; + let input = "hello"; + let mut buf = vec![99; 20]; + let w1 = wireformat::write_ssh(&mut buf, &input).unwrap(); + buf.truncate(w1); + + let mut hash_ctx = digest::Context::new(&digest::SHA256); + wireformat::hash_ssh(&mut hash_ctx, &input).unwrap(); + let digest1 = hash_ctx.finish(); + let digest2 = digest::digest(&digest::SHA256, &buf); + assert_eq!(digest1.as_ref(), digest2.as_ref()); + } } diff --git a/door-tokio/Cargo.toml b/door-tokio/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f6497792d4d67261be2c0a74cd44a16c169a7558 --- /dev/null +++ b/door-tokio/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "door-tokio" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.17", features = ["full"] } +door-sshproto = { path = "../door-sshproto", features = ["std"] } +log = { version = "0.4" } + +[dev-dependencies] +snafu = { version = "0.7", default-features = true } +anyhow = { version = "1.0" } +pretty-hex = "0.2" +simplelog = "0.11" diff --git a/door-tokio/examples/con1.rs b/door-tokio/examples/con1.rs new file mode 100644 index 0000000000000000000000000000000000000000..ebdd0f47a999e8fcdb28a6e7ae55de99fd3c456c --- /dev/null +++ b/door-tokio/examples/con1.rs @@ -0,0 +1,64 @@ +#[allow(unused_imports)] +use { + // crate::error::Error, + log::{debug, error, info, log, trace, warn}, +}; +use std::error::Error; +use pretty_hex::PrettyHex; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; + +use door_sshproto::*; + +use simplelog::*; + +#[tokio::main] +async fn main() -> Result<(), Box<dyn Error>> { + + CombinedLogger::init( + vec![ + TermLogger::new(LevelFilter::Trace, Config::default(), TerminalMode::Mixed, ColorChoice::Auto), + ] + ).unwrap(); + + info!("running main"); + trace!("tracing main"); + + // Connect to a peer + let mut stream = TcpStream::connect("dropbear.nl:22").await?; + + let mut work = vec![0; 1000]; + let c = conn::Conn::new(); + let mut r = conn::Runner::new(c, work.as_mut_slice()); + + let mut buf = vec![0; 100]; + loop { + stream.readable().await?; + let n = stream.try_read(&mut buf)?; + let s = &buf.as_slice()[..n]; + let l = r.input(s)?; + println!("read {l}"); + } + + + // let mut d = ident::RemoteVersion::new(); + // let (taken, done) = d.consume(&buf)?; + // println!("taken {taken} done {done}"); + // let v = d.version(); + // match v { + // Some(x) => { + // println!("v {:?}", x.hex_dump()); + // } + // None => { + // println!("None"); + // } + // } + // let (_, rest) = buf.split_at(taken + 5); + // println!("reset {:?}", rest.hex_dump()); + + // let ctx = packets::ParseContext::new(); + // let p = wireformat::packet_from_bytes(rest, &ctx)?; + // println!("{p:#?}"); + + Ok(()) +} diff --git a/door-tokio/src/lib.rs b/door-tokio/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..1b4a90c938387450422982a89aed075aa1e8c010 --- /dev/null +++ b/door-tokio/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +}