From 7b50417ba770b2d1e2edf7bc3c54c052a6f4f5e7 Mon Sep 17 00:00:00 2001 From: Michael Pfaff Date: Sun, 11 Jun 2023 00:10:07 -0400 Subject: [PATCH] WIP public key authentication - Implemented public key authentication - TODO: figure out key selection (I refuse to resort to sending all public keys to the server) - Refactoring --- Cargo.lock | 718 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 17 +- src/auth/mod.rs | 86 ++++++ src/auth/password.rs | 219 +++++++++++++ src/auth/ssh_key.rs | 119 +++++++ src/main.rs | 432 +++++++------------------- 6 files changed, 1241 insertions(+), 350 deletions(-) create mode 100644 src/auth/mod.rs create mode 100644 src/auth/password.rs create mode 100644 src/auth/ssh_key.rs diff --git a/Cargo.lock b/Cargo.lock index 67abc94..6356fbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.0.2" @@ -11,6 +22,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -43,6 +69,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base64" version = "0.13.1" @@ -55,6 +87,23 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3806a8db60cf56efee531616a34a6aaa9a114d6da2add861b0fa4a188881b2c7" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2 0.10.6", +] + [[package]] name = "bindgen" version = "0.59.2" @@ -84,6 +133,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -93,10 +151,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "brisk" version = "0.1.0" -source = "git+https://git.pfaff.dev/michael/brisk.git#41172e3adbde6de4727b1117a6e0cf631877cc99" [[package]] name = "bumpalo" @@ -143,6 +210,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -169,6 +259,12 @@ dependencies = [ "vec_map", ] +[[package]] +name = "const-oid" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" + [[package]] name = "core-foundation" version = "0.9.3" @@ -194,6 +290,18 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -204,14 +312,93 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", + "const-oid", "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", ] [[package]] @@ -220,6 +407,25 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "enum-repr" version = "0.2.6" @@ -247,12 +453,21 @@ dependencies = [ [[package]] name = "fast-hex" version = "0.1.0" -source = "git+https://git.pfaff.dev/michael/fast-hex.git#9daecc56ea29fe2b500cd172759be707fe375943" dependencies = [ "brisk", "cache-padded", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "flume" version = "0.10.14" @@ -288,6 +503,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -297,7 +523,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -307,6 +533,23 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -325,12 +568,76 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "js-sys" version = "0.3.63" @@ -345,6 +652,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lazycell" @@ -368,6 +678,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "lock_api" version = "0.4.10" @@ -421,7 +737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -431,7 +747,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom", + "getrandom 0.2.10", ] [[package]] @@ -468,6 +784,45 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -475,6 +830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -493,6 +849,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -506,15 +868,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] -name = "pam-client" -version = "0.5.0" +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "bitflags", - "enum-repr", - "libc", - "pam-sys", - "rustversion", - "serde", + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", +] + +[[package]] +name = "p384" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", ] [[package]] @@ -570,6 +942,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -585,6 +966,15 @@ dependencies = [ "base64 0.13.1", ] +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "pin-project" version = "1.1.0" @@ -617,6 +1007,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -656,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85af4ed6ee5a89f26a26086e9089a6643650544c025158449a3626ebf72884b3" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring", "rustc-hash", "rustls", @@ -691,22 +1103,27 @@ dependencies = [ "flume", "libc", "nix", - "pam-client 0.5.0", - "pam-client 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pam-client", "parking_lot", "pin-project-lite", "quinn", + "rand 0.8.5", "rcgen", + "rmp", "rmp-serde", + "rmpv", "rpassword", "rustls", "rustls-webpki", "serde", - "sha2", + "serde_with", + "sha2 0.10.6", + "ssh-key", "tokio", "tracing", "tracing-subscriber", "triggered", + "zeroize", ] [[package]] @@ -718,6 +1135,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -725,8 +1155,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -736,7 +1176,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -745,7 +1194,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -801,6 +1259,17 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + [[package]] name = "ring" version = "0.16.20" @@ -838,6 +1307,16 @@ dependencies = [ "serde", ] +[[package]] +name = "rmpv" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de8813b3a2f95c5138fe5925bfb8784175d88d6bff059ba8ce090aa891319754" +dependencies = [ + "num-traits", + "rmp", +] + [[package]] name = "rpassword" version = "7.2.0" @@ -849,6 +1328,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rtoolbox" version = "0.0.1" @@ -913,6 +1413,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "schannel" version = "0.1.21" @@ -938,6 +1444,20 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.1" @@ -981,6 +1501,45 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" +dependencies = [ + "base64 0.21.2", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.6" @@ -989,7 +1548,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -1016,6 +1575,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.8" @@ -1066,6 +1635,49 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.6", +] + +[[package]] +name = "ssh-key" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288d8f5562af5a3be4bda308dd374b2c807b940ac370b5efa1c99311da91d9a1" +dependencies = [ + "aes", + "bcrypt-pbkdf", + "ctr", + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2 0.10.6", + "signature", + "ssh-encoding", + "zeroize", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1078,6 +1690,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -1154,8 +1772,10 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ + "itoa", "serde", "time-core", + "time-macros", ] [[package]] @@ -1164,6 +1784,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1319,6 +1948,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1431,6 +2066,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -1563,6 +2207,34 @@ dependencies = [ "time", ] +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[patch.unused]] +name = "chow" +version = "0.2.0" + [[patch.unused]] name = "how" version = "0.3.0" + +[[patch.unused]] +name = "minify-html-onepass" +version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 63b0c7d..5b8fb40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,14 @@ version = "0.1.0" edition = "2021" [features] -server = ["dep:pam-client", "dep:pam-client-macos"] +default = ["ed25519", "ecdsa", "rsa"] + +#server = ["dep:pam-client", "dep:pam-client-macos"] +server = ["dep:pam-client"] + +ed25519 = ["ssh-key/ed25519"] +ecdsa = ["ssh-key/ecdsa"] +rsa = ["ssh-key/rsa"] [dependencies] anyhow = "1.0.71" @@ -17,21 +24,27 @@ nix = "0.26.2" parking_lot = "0.12.1" pin-project-lite = "0.2.9" quinn = "0.10.1" +rand = "0.8.5" rcgen = "0.10.0" +rmp = "0.8.11" rmp-serde = "1.1.1" +rmpv = "1.0.0" rpassword = "7.2.0" rustls = { version = "0.21.1", default-features = false, features = ["dangerous_configuration"] } rustls-webpki = "0.100.1" serde = { version = "1.0.163", features = ["derive"] } +serde_with = { version = "3.0.0", default-features = false, features = ["std"] } sha2 = "0.10.6" +ssh-key = { version = "0.5.1", default-features = false, features = ["std", "encryption"] } #termion = "2.0.1" tokio = { version = "1.28.2", default-features = false, features = ["rt-multi-thread", "macros", "process", "io-util", "io-std", "time", "fs", "signal"] } tracing = { version = "0.1.37", features = ["max_level_debug", "release_max_level_info"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } triggered = "0.1.2" +zeroize = { version = "1.6.0", features = ["std"] } [target.'cfg(not(target_os = "macos"))'.dependencies] pam-client = { version = "0.5.0", default-features = false, features = ["serde"], optional = true } [target.'cfg(target_os = "macos")'.dependencies] -pam-client-macos = { package = "pam-client", version = "0.5.0", path = "../../../../../Users/michael/b/rust-pam-client", default-features = false, features = ["serde"], optional = true } +#pam-client-macos = { package = "pam-client", version = "0.5.0", path = "../../../../../Users/michael/b/rust-pam-client", default-features = false, features = ["serde"], optional = true } diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..99d3318 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,86 @@ +pub mod password; +pub mod ssh_key; + +use std::ffi::CStr; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hello<'a> { + #[serde(borrow)] + pub username: &'a str, + #[serde(borrow)] + pub auth_method: Method<'a>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Method<'a> { + Password, + SshKey { + /// [Public key](::ssh_key::public::PublicKey). + #[serde(borrow)] + public_key: &'a str, + }, +} + +mod cstr_as_bytes { + use std::ffi::CStr; + + use serde::{Serializer, Deserializer, Deserialize}; + + pub fn serialize(s: &CStr, ser: S) -> Result where S: Serializer { + ser.serialize_bytes(s.to_bytes_with_nul()) + } + + pub fn deserialize<'de, D>(de: D) -> Result<&'de CStr, D::Error> where D: Deserializer<'de> { + use serde::de::Error; + let b = <&'de [u8]>::deserialize(de)?; + CStr::from_bytes_with_nul(b) + .map_err(|_| D::Error::invalid_value(serde::de::Unexpected::Bytes(b), &"a sequence of bytes ending with NUL")) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +struct ByteSlice<'a>(#[serde(serialize_with = "serialize_bytes")] &'a [u8]); + +pub fn serialize_bytes(b: &[u8], ser: S) -> Result where S: serde::Serializer { + ser.serialize_bytes(b) +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CStrAsBytes<'a>(#[serde(serialize_with = "cstr_as_bytes::serialize", deserialize_with = "cstr_as_bytes::deserialize", borrow)] &'a CStr); + +impl<'a> std::ops::Deref for CStrAsBytes<'a> { + type Target = CStr; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a> From<&'a CStr> for CStrAsBytes<'a> { + fn from(value: &'a CStr) -> Self { + Self(value) + } +} + +impl<'a> From> for &'a CStr { + fn from(value: CStrAsBytes<'a>) -> Self { + value.0 + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Question<'a> { + Prompt { #[serde(borrow)] prompt: CStrAsBytes<'a>, echo: bool }, + TextInfo(#[serde(borrow)] CStrAsBytes<'a>), + ErrorMsg(#[serde(borrow)] CStrAsBytes<'a>), + //SshAuthRequest(ssh_key::AuthRequest<'a>), + LoggedIn, +} + +#[derive(Debug, Serialize, Deserialize)] +enum Answer<'a> { + Prompt(#[serde(borrow)] CStrAsBytes<'a>), + //SshAuthResponse(ssh_key::AuthResponse<'a>), +} diff --git a/src/auth/password.rs b/src/auth/password.rs new file mode 100644 index 0000000..8210b7c --- /dev/null +++ b/src/auth/password.rs @@ -0,0 +1,219 @@ +use std::{ffi::CString, mem::ManuallyDrop, os::fd::FromRawFd}; + +use anyhow::{Context, Result}; +use quinn::{SendStream, RecvStream}; +use tokio::io::AsyncWriteExt; +use zeroize::Zeroizing; + +use crate::{write_msg, read_msg}; +use super::{Question, Answer}; + +#[cfg(feature = "server")] +pub async fn server_authenticate(send: &mut SendStream, recv: &mut RecvStream, username: String) -> Result<()> { + use pam_client::ConversationHandler; + + use crate::Message; + + use super::*; + + let (q_send, q_recv) = flume::bounded(1); + let (a_send, a_recv) = flume::bounded(1); + + struct Conversation { + send: flume::Sender, + recv: flume::Receiver, + error: std::cell::Cell>, + } + + impl Conversation { + const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); + + fn error(&self, msg: &'static str) -> pam_client::ErrorCode { + self.error.set(Some(msg)); + pam_client::ErrorCode::CONV_ERR + } + + fn ask(&self, question: Question) -> Result<(), pam_client::ErrorCode> { + self.send + .send_timeout(Message::from_value(&question) + .map_err(|_| self.error("Serialization error"))?, Self::TIMEOUT) + .map_err(|_| self.error("Ask question timed out")) + } + + fn prompt(&self, prompt: &CStr, echo: bool) -> Result { + self.ask(Question::Prompt { + prompt: prompt.into(), + echo, + })?; + self.recv + .recv_timeout(Self::TIMEOUT) + .map_err(|_| self.error("Wait for answer timed out")) + } + } + + impl ConversationHandler for Conversation { + fn prompt_echo_on( + &mut self, + prompt: &CStr, + ) -> std::result::Result { + self.prompt(prompt, true) + } + + fn prompt_echo_off( + &mut self, + prompt: &CStr, + ) -> std::result::Result { + self.prompt(prompt, false) + } + + fn text_info(&mut self, msg: &CStr) { + _ = self.ask(Question::TextInfo(msg.into())); + } + + fn error_msg(&mut self, msg: &CStr) { + _ = self.ask(Question::ErrorMsg(msg.into())); + } + } + + let hdl = tokio::task::spawn_blocking(move || { + let mut ctx = pam_client::Context::new( + "sshd", + Some(&username), + Conversation { + send: q_send, + recv: a_recv, + error: Default::default(), + }, + )?; + info!("created context"); + + ctx.authenticate(pam_client::Flag::NONE) + .with_context(|| ctx.conversation_mut().error.take().unwrap_or("Unknown error")) + .context("authenticate")?; + info!("authenticated user"); + + // very odd behaviour: + // if user is currently logged in, we must call acct_mgmt first. + // if user is not currently logged in, we must call acc_mgmt first and ignore the error. + if let Err(e) = ctx.acct_mgmt(pam_client::Flag::NONE).context("acct_mgmt") { + if cfg!(any(target_os = "macos", target_os = "ios")) { + warn!("ignoring validation error due to macOS oddity: {}", e); + } else { + return Err(e).with_context(|| ctx.conversation_mut().error.take().unwrap_or("Unknown error")); + } + } + info!("validated user"); + + /*let sess = ctx + .open_session(pam_client::Flag::NONE) + .context("open_session")?; + info!("opened session"); + let sess = sess.leak();*/ + + let conv = ctx.conversation_mut(); + conv.send = flume::bounded(0).0; + conv.recv = flume::bounded(0).1; + //Result::<_>::Ok((ctx, sess)) + Result::<_>::Ok((ctx, ())) + }); + + let mut answer_buf = Vec::new(); + while let Ok(question_msg) = q_recv.recv_async().await { + let question: Question = question_msg.to_value().unwrap(); + debug!("received question: {:?}", question); + question_msg.write_ref(send).await?; + if matches!(question, Question::Prompt { .. }) { + let answer = read_msg(recv, &mut answer_buf).await??; + trace!("received answer: {:?}", answer); + let answer = match answer { + Answer::Prompt(s) => (*s).to_owned(), + //Answer::SshAuthResponse(_) => bail!("The received answer was unexpected: SshAuthResponse"), + }; + a_send.send_async(answer).await?; + } + } + + let (_ctx, _sess) = hdl.await??; + /*let sess = ctx.unleak_session(sess); + let env = sess.envlist(); + let env = env + .into_iter() + .filter_map(|pair| { + let element = pair.as_cstr().to_bytes_with_nul(); + let sep = element + .iter() + .position(|b| *b == b'=') + .unwrap_or(element.len()); + let k = CString::new(&element[..sep]).ok()?; + let v = CString::new(&element[sep + 1..]).ok()?; + Some((k, v)) + }) + .collect::>();*/ + Ok(()) +} + +pub async fn client_authenticate( + conn: &quinn::Connection, + send: &mut SendStream, + recv: &mut RecvStream, + username: &str, +) -> Result<()> { + write_msg( + send, + &super::Hello { + username, + auth_method: super::Method::Password, + }, + ) + .await?; + + let mut stdout = + unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDOUT_FILENO)) }; + + let mut msg_buf = Vec::new(); + loop { + tokio::select! { + r = read_msg::(recv, &mut msg_buf) => { + match r?? { + Question::LoggedIn => { + return Ok(()); + } + Question::Prompt { + prompt, + echo, + } => { + stdout.write_all(prompt.to_bytes()).await?; + stdout.write_all(b" ").await?; + let answer = rpassword::read_password()?; + let answer = Zeroizing::new(CString::new(answer)?); + write_msg(send, &Answer::Prompt((*answer).as_ref().into())).await?; + }, + //Question::SshAuthRequest(_) => bail!("Received an unexpected question from the server: SshAuthRequest"), + Question::TextInfo(s) => { + stdout.write_all(b"INFO ").await?; + stdout.write_all(s.to_bytes()).await?; + stdout.write_all(b"\n").await?; + }, + Question::ErrorMsg(s) => { + stdout.write_all(b"ERRO ").await?; + stdout.write_all(s.to_bytes()).await?; + stdout.write_all(b"\n").await?; + }, + } + } + r = send.stopped() => { + info!("Remote disconnected"); + let code = r?.into_inner(); + if code == 0 { + return Ok(()); + } else { + return Err(anyhow!("Error code {}", code)); + } + } + e = conn.closed() => { + info!("Remote disconnected: {}", e); + return Err(anyhow!("Remote connection closed")); + } + } + } +} diff --git a/src/auth/ssh_key.rs b/src/auth/ssh_key.rs new file mode 100644 index 0000000..60557b5 --- /dev/null +++ b/src/auth/ssh_key.rs @@ -0,0 +1,119 @@ +use anyhow::{Context, Result}; +use nix::unistd::Uid; +use quinn::{SendStream, RecvStream}; +use zeroize::Zeroizing; + +use crate::{read_msg, write_msg, ClientConfig, Message}; +use super::Question; + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthRequest<'a> { + #[serde(serialize_with = "super::serialize_bytes")] + pub nonce: &'a [u8], +} + +impl<'a> AuthRequest<'a> { + pub const NAMESPACE: &str = "QUINOA AUTHENTICATION"; + + pub fn new(nonce: &'a [u8]) -> AuthRequest<'a> { + Self { + nonce, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthResponse<'a> { + #[serde(borrow)] + signature: &'a str, +} + +#[cfg(feature = "server")] +async fn server_authorize_key(user_id: Uid, public_key: &ssh_key::public::PublicKey) -> Result<()> { + const PATH: &str = "/.ssh/authorized_keys"; + + let mut user_passwd_buf = Vec::new(); + let user_passwd = crate::passwd::Passwd::from_uid(user_id, &mut user_passwd_buf)? + .context("No passwd entry for user")?; + + let home = user_passwd.dir.to_str()?; + + let mut authorized_keys = String::with_capacity(home.len() + PATH.len()); + authorized_keys.push_str(home); + authorized_keys.push_str(PATH); + let authorized_keys = tokio::fs::read_to_string(authorized_keys).await?; + let mut authorized_keys = ssh_key::authorized_keys::AuthorizedKeys::new(&authorized_keys); + + while let Some(r) = authorized_keys.next() { + let entry = r?; + if entry.public_key().key_data() == public_key.key_data() { + return Ok(()); + } + } + Err(anyhow!("Key provided by the client is not authorized to connect")) +} + +#[cfg(feature = "server")] +pub async fn server_authenticate(send: &mut SendStream, recv: &mut RecvStream, user_id: Uid, public_key: &str) -> Result<()> { + use rand::RngCore; + + let public_key = ssh_key::public::PublicKey::from_openssh(public_key)?; + + server_authorize_key(user_id, &public_key).await?; + + let mut nonce = vec![0; 256]; + rand::thread_rng().try_fill_bytes(&mut nonce)?; + let request = AuthRequest::new(&nonce); + let request = Message::from_value(&request)?; + request.write_ref(send).await?; + + let mut buf = Vec::new(); + let response = read_msg::(recv, &mut buf).await??; + + let sig = ssh_key::SshSig::from_pem(&response.signature)?; + public_key.verify(AuthRequest::NAMESPACE, &request.0, &sig)?; + + Ok(()) +} + +pub async fn client_authenticate(cfg: &ClientConfig, send: &mut SendStream, recv: &mut RecvStream, username: &str) -> Result<()> { + let private_key = Zeroizing::new(tokio::fs::read_to_string(&cfg.ssh_key_file).await?); + let private_key = ssh_key::private::PrivateKey::from_openssh(private_key) + .context("Loading private key")?; + let public_key = private_key.public_key(); + let public_key = public_key.to_openssh() + .context("Encoding public key")?; + info!("loaded private key: {:?}", private_key.algorithm()); + + write_msg(send, &super::Hello { + username, + auth_method: super::Method::SshKey { + public_key: &public_key, + }, + }).await?; + info!("sent hello"); + + let mut buf = Vec::new(); + _ = read_msg::(recv, &mut buf).await??; + info!("read auth request"); + + let private_key = if private_key.is_encrypted() { + let password = Zeroizing::new(rpassword::prompt_password("Enter the key's passphrase: ")?); + private_key.decrypt(&password).context("Incorrect passphrase")? + } else { + private_key + }; + + let sig = private_key.sign(AuthRequest::NAMESPACE, ssh_key::HashAlg::Sha512, &buf)?; + let sig = sig.to_pem(ssh_key::LineEnding::LF)?; + info!("signed auth request"); + + write_msg(send, &AuthResponse { signature: &sig }).await?; + info!("wrote auth request"); + + let Question::LoggedIn = read_msg(recv, &mut buf).await?? else { + bail!("Received an unexpected question") + }; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index a3b9605..8f19a05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ extern crate serde; #[macro_use] extern crate tracing; +mod auth; mod io_util; mod passwd; mod pty; @@ -35,8 +36,6 @@ use std::task::Poll; use anyhow::{Context, Result}; use base64::Engine as _; use nix::unistd::Uid; -#[cfg(feature = "server")] -use pam_client::ConversationHandler; use quinn::{ReadExactError, RecvStream, SendStream}; use rustls::client::ServerCertVerifier; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -220,13 +219,14 @@ async fn run_cmd(mut args: std::env::Args) -> Result<()> { const ALPN_QUINOA: &str = "quinoa"; -struct ServerConfig { +pub struct ServerConfig { listen: SocketAddr, } -struct ClientConfig { +pub struct ClientConfig { known_hosts_file: String, known_hosts: parking_lot::Mutex>>, + ssh_key_file: String, } #[cfg(feature = "server")] @@ -239,7 +239,8 @@ async fn run_server() -> Result<()> { &*Box::leak(ServerConfig { listen: opt_listen }.into()) }; - let subject_alt_names = vec!["localhost".to_string()]; + //let subject_alt_names = vec!["localhost".to_string()]; + let subject_alt_names = vec![]; let (cert, key) = if !std::path::Path::new("cert.der").exists() || !std::path::Path::new("key.der").exists() @@ -313,8 +314,10 @@ impl fmt::Display for FinishedEarly { impl std::error::Error for FinishedEarly {} -async fn read_msg( +/// Reads a message `T`. `buf` will be cleared before reading. +async fn read_msg<'a, T: serde::Deserialize<'a>>( recv: &mut RecvStream, + buf: &'a mut Vec, ) -> Result> { let mut size = [0u8; 2]; match recv.read_exact(&mut size).await { @@ -323,19 +326,59 @@ async fn read_msg( Err(ReadExactError::ReadError(e)) => return Err(e.into()), } let size = u16::from_le_bytes(size); - let mut buf = Vec::with_capacity(size.into()); - recv.take(size.into()).read_to_end(&mut buf).await?; - Ok(Ok(rmp_serde::from_slice(&buf).with_context(|| { - format!("reading a {} byte message", size) - })?)) + buf.clear(); + buf.reserve(size.into()); + recv.take(size.into()).read_to_end(buf).await?; + Message::raw_to_value(buf).map(Ok) +} + +struct Message(Vec); + +impl Message { + pub fn from_value(value: &T) -> Result { + Ok(Self(rmp_serde::to_vec_named(value)?)) + } + + pub fn from_raw(data: Vec) -> Self { + Self(data) + } + + async fn write_len(&self, send: &mut SendStream) -> Result<()> { + send.write_all(&u16::try_from(self.0.len())?.to_le_bytes()) + .await + .map_err(|e| e.into()) + } + + pub async fn write_ref(&self, send: &mut SendStream) -> Result<()> { + self.write_len(send).await?; + send.write_all(&self.0).await?; + Ok(()) + } + + pub async fn write(self, send: &mut SendStream) -> Result<()> { + self.write_len(send).await?; + send.write_chunk(self.0.into()).await?; + Ok(()) + } + + fn raw_to_value<'de, T: serde::Deserialize<'de>>(data: &'de [u8]) -> Result { + rmp_serde::from_slice(data) + .with_context(|| { + format!("reading a {} byte message", data.len()) + }) + .with_context(|| { + let mut data = data; + format!("{:?}", rmpv::decode::read_value_ref(&mut data)) + }) + } + + pub fn to_value<'de, T: serde::Deserialize<'de>>(&'de self) -> Result { + Self::raw_to_value(&self.0) + } } async fn write_msg(send: &mut SendStream, value: &T) -> Result<()> { - let buf = rmp_serde::to_vec(value)?; - send.write_all(&u16::try_from(buf.len())?.to_le_bytes()) - .await?; - send.write_all(&buf).await?; - Ok(()) + Message::from_value(value)?.write(send).await } struct InformedServerCertVerifier { @@ -364,8 +407,14 @@ impl InformedServerCertVerifier { let hash = Hash::new(&end_entity.0); eprintln!( - "The authenticity of host {:?} can't be established.", - subject_name + "The authenticity of host {} can't be established.", + match subject_name { + SubjectNameRef::DnsName(name) => name.into(), + SubjectNameRef::IpAddress(ip) => std::str::from_utf8(match ip { + webpki::IpAddrRef::V4(b, _) => b, + webpki::IpAddrRef::V6(b, _) => b, + }).map_err(|e| CertificateError::Other(Arc::new(e)))?, + } ); eprintln!("Certificate hash is {}", hash); if let Some(known_as) = known_as.and_then(|i| known_hosts.get(i)) { @@ -479,8 +528,10 @@ impl ServerCertVerifier for InformedServerCertVerifier { _ => return Err(CertificateError::NotValidForName.into()), }; - cert.verify_is_valid_for_subject_name(subject_name) - .map_err(pki_error)?; + // TODO: is expiry checked for us? + + /*cert.verify_is_valid_for_subject_name(subject_name) + .map_err(pki_error)?;*/ let mut known_hosts = self.cfg.known_hosts.lock(); if let Some((h_index, h)) = Self::find_known_host(&known_hosts, subject_name) { @@ -648,8 +699,8 @@ fn write_known_hosts(file: &str, hosts: &[KnownHost<'_>]) -> Result<()> { async fn run_client(mut args: std::env::Args) -> Result<()> { info!("running client"); - let mut cfg_dir = std::env::var("HOME")?; - cfg_dir.push_str("/.config/quinoa"); + let home_dir = std::env::var("HOME")?; + let cfg_dir = format!("{}/.config/quinoa", home_dir); tokio::fs::create_dir_all(&cfg_dir).await?; let known_hosts_file = format!("{}/known_hosts", cfg_dir); let known_hosts = if std::path::Path::new(&known_hosts_file).exists() { @@ -665,19 +716,26 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { } else { Vec::new() }; + let ssh_key_file = format!("{}/.ssh/id_ed25519", home_dir); + //let ssh_key_file = format!("{}/.ssh/id_rsa", home_dir); let cfg = &*Box::leak( ClientConfig { known_hosts_file, known_hosts: known_hosts.into(), + ssh_key_file, } .into(), ); let mut conn_str = None; let mut forwards = Vec::new(); + let mut use_key = true; while let Some(arg) = args.next() { if let Some(arg) = arg.strip_prefix('-') { match arg { + "-no-key" => { + use_key = false; + } "-forward" => { let v = args.next().context("Expected a value of the form LOCAL_ADDR:LOCAL_PORT[->|<-]REMOTE_ADDR:REMOTE_PORT (and /[tcp|udp] on bind side)")?; forwards.push(v.parse::()?); @@ -695,6 +753,8 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { .as_ref() .and_then(|s| s.split_once('@')) .context("Expected an argument of the form USERNAME@HOST")?; + let (host_name, port) = host.split_once(':').unwrap_or((host, "8022")); + let port = port.parse::()?; let mut client_crypto = rustls::ClientConfig::builder() .with_safe_defaults() @@ -714,20 +774,17 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { info!("connecting"); - let conn = endpoint.connect(host.parse()?, "localhost")?.await?; + let conn = endpoint.connect(host.parse()?, host_name)?.await?; // authenticating client { let (mut send, mut recv) = conn.open_bi().await?; - write_msg( - &mut send, - &auth::Hello { - username: username.to_owned(), - }, - ) - .await?; - do_auth_prompt(&conn, &mut send, &mut recv).await?; + if use_key { + auth::ssh_key::client_authenticate(cfg, &mut send, &mut recv, username).await?; + } else { + auth::password::client_authenticate(&conn, &mut send, &mut recv, username).await?; + } } // authenticated client @@ -954,63 +1011,6 @@ async fn do_shell(conn: &quinn::Connection) -> Result<()> { } } -async fn do_auth_prompt( - conn: &quinn::Connection, - send: &mut SendStream, - recv: &mut RecvStream, -) -> Result<()> { - use auth::*; - - let mut stdout = - unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDOUT_FILENO)) }; - - loop { - tokio::select! { - r = read_msg::(recv) => { - match r?? { - Question::LoggedIn => { - return Ok(()); - } - Question::Prompt { - prompt, - echo, - } => { - let mut prompt = prompt.into_bytes(); - prompt.push(b' '); - stdout.write_all(&prompt).await?; - let answer = rpassword::read_password()?; - let answer = CString::new(answer)?; - write_msg(send, &Answer::Prompt(answer)).await?; - }, - Question::TextInfo(s) => { - stdout.write_all(b"INFO ").await?; - stdout.write_all(s.as_bytes()).await?; - stdout.write_all(b"\n").await?; - }, - Question::ErrorMsg(s) => { - stdout.write_all(b"ERRO ").await?; - stdout.write_all(s.as_bytes()).await?; - stdout.write_all(b"\n").await?; - }, - } - } - r = send.stopped() => { - info!("Remote disconnected"); - let code = r?.into_inner(); - if code == 0 { - return Ok(()); - } else { - return Err(anyhow!("Error code {}", code)); - } - } - e = conn.closed() => { - info!("Remote disconnected: {}", e); - return Err(anyhow!("Remote connection closed")); - } - } - } -} - #[cfg(feature = "server")] async fn greet_conn(cfg: &'static ServerConfig, conn: quinn::Connecting) -> Result<()> { info!("greeting connection"); @@ -1046,250 +1046,31 @@ async fn greet_conn(cfg: &'static ServerConfig, conn: quinn::Connecting) -> Resu Ok(()) } -mod auth { - use std::ffi::CString; - - #[derive(Debug, Serialize, Deserialize)] - pub struct Hello { - pub username: String, - } - - #[derive(Debug, Serialize, Deserialize)] - pub enum Question { - Prompt { prompt: CString, echo: bool }, - TextInfo(CString), - ErrorMsg(CString), - LoggedIn, - } - - #[derive(Debug, Serialize, Deserialize)] - pub enum Answer { - Prompt(CString), - } -} - #[cfg(feature = "server")] async fn authenticate_conn( cfg: &'static ServerConfig, conn: &quinn::Connection, ) -> Result<(UserInfo, Vec<(CString, CString)>)> { - use auth::*; - info!("authenticating connection"); let (mut send, mut recv) = conn.accept_bi().await?; - let hello = read_msg::(&mut recv).await??; + let mut hello_buf = Vec::new(); + let hello = read_msg::(&mut recv, &mut hello_buf).await??; - let (q_send, q_recv) = flume::bounded(1); - let (a_send, a_recv) = flume::bounded(1); + let user_info = user_info::get_user_info(&hello.username).await?; - struct Conversation { - send: flume::Sender, - recv: flume::Receiver, + match hello.auth_method { + auth::Method::Password => auth::password::server_authenticate(&mut send, &mut recv, hello.username.to_owned()).await?, + auth::Method::SshKey { public_key } => auth::ssh_key::server_authenticate(&mut send, &mut recv, user_info.user.id, public_key).await?, } - - impl Conversation { - const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); - - fn ask(&self, question: Question) -> Result<(), pam_client::ErrorCode> { - self.send - .send_timeout(question, Self::TIMEOUT) - .map_err(|_| pam_client::ErrorCode::ABORT) - } - - fn answer(&self) -> Result { - self.recv - .recv_timeout(Self::TIMEOUT) - .map_err(|_| pam_client::ErrorCode::ABORT) - } - } - - impl ConversationHandler for Conversation { - fn prompt_echo_on( - &mut self, - prompt: &CStr, - ) -> std::result::Result { - self.ask(Question::Prompt { - prompt: prompt.to_owned(), - echo: true, - })?; - match self.answer()? { - Answer::Prompt(s) => Ok(s), - } - } - - fn prompt_echo_off( - &mut self, - prompt: &CStr, - ) -> std::result::Result { - self.ask(Question::Prompt { - prompt: prompt.to_owned(), - echo: false, - })?; - match self.answer()? { - Answer::Prompt(s) => Ok(s), - } - } - - fn text_info(&mut self, msg: &CStr) { - _ = self.ask(Question::TextInfo(msg.to_owned())); - } - - fn error_msg(&mut self, msg: &CStr) { - _ = self.ask(Question::ErrorMsg(msg.to_owned())); - } - } - - let username = hello.username.clone(); - let hdl = tokio::task::spawn_blocking(move || { - let mut ctx = pam_client::Context::new( - "sshd", - Some(&username), - Conversation { - send: q_send, - recv: a_recv, - }, - )?; - info!("created context"); - - ctx.authenticate(pam_client::Flag::NONE) - .context("authenticate")?; - info!("authenticated user"); - - // very odd behaviour: - // if user is currently logged in, we must call acct_mgmt first. - // if user is not currently logged in, we must call acc_mgmt first and ignore the error. - if let Err(e) = ctx.acct_mgmt(pam_client::Flag::NONE).context("acct_mgmt") { - if cfg!(any(target_os = "macos", target_os = "ios")) { - warn!("ignoring validation error due to macOS oddity: {}", e); - } else { - return Err(e); - } - } - info!("validated user"); - - /*let sess = ctx - .open_session(pam_client::Flag::NONE) - .context("open_session")?; - info!("opened session"); - let sess = sess.leak();*/ - - let conv = ctx.conversation_mut(); - conv.send = flume::bounded(0).0; - conv.recv = flume::bounded(0).1; - //Result::<_>::Ok((ctx, sess)) - Result::<_>::Ok((ctx, ())) - }); - - while let Ok(question) = q_recv.recv_async().await { - debug!("received question: {:?}", question); - write_msg(&mut send, &question).await?; - if matches!(question, Question::Prompt { .. }) { - let answer = read_msg(&mut recv).await??; - trace!("received answer: {:?}", answer); - a_send.send_async(answer).await?; - } - /*match question { - Question::Prompt { prompt, echo } => { - let r = async { - // FIXME: actually disable echo - send.write_all(prompt.as_bytes()).await?; - send.write_all(b" ").await?; - let erase = format!("\x1b[{}G\x1b[K", prompt.as_bytes().len() + 1 + 1); - let mut buf = Vec::new(); - 'prompt: loop { - let mut i = buf.len(); - recv.read_buf(&mut buf).await?; - let mut j = i; - while j < buf.len() { - match buf[j] { - 0x7f => { - buf.remove(j); - if j > 0 { - if j == i { - i -= 1; - } - buf.remove(j-1); - // erase in line, move cursor left 1 column - send.write_all(erase.as_bytes()).await?; - send.write_all(&buf).await?; - j -= 1; - } - } - 0x3 => { - send.write_all(b"\r\n").await?; - return Err(anyhow!("Aborted by the user")); - } - b'\r' => { - buf.remove(j); - } - b'\n' => { - info!("found \\n"); - // remove newline and trailing chars - buf.truncate(j); - break 'prompt; - } - _ => { - j += 1; - } - } - } - let seg = &buf[i..]; - if echo { - send.write_all(&seg).await?; - } else { - send.write_all(&vec![b'*'; seg.len()]).await?; - } - info!("{:?} ({:x?})", std::str::from_utf8(&buf), buf); - } - let buf = CString::new(buf)?; - send.write_all(b"\n").await?; - Result::<_>::Ok(buf) - }.await; - a_send.send_async(Answer::Prompt(r.map_err(|e| { - error!("PAM error: {}", e); - pam_client::ErrorCode::ABORT - }))).await?; - } - Question::TextInfo(s) => { - send.write_all(b"INFO ").await?; - send.write_all(s.as_bytes()).await?; - send.write_all(b"\n").await?; - } - Question::ErrorMsg(s) => { - send.write_all(b"ERRO ").await?; - send.write_all(s.as_bytes()).await?; - send.write_all(b"\n").await?; - } - }*/ - } - - let (mut ctx, sess) = hdl.await??; - /*let sess = ctx.unleak_session(sess); - let env = sess.envlist(); - let env = env - .into_iter() - .filter_map(|pair| { - let element = pair.as_cstr().to_bytes_with_nul(); - let sep = element - .iter() - .position(|b| *b == b'=') - .unwrap_or(element.len()); - let k = CString::new(&element[..sep]).ok()?; - let v = CString::new(&element[sep + 1..]).ok()?; - Some((k, v)) - }) - .collect::>();*/ let env = Vec::new(); info!("logged in"); - write_msg(&mut send, &Question::LoggedIn).await?; + write_msg(&mut send, &auth::Question::LoggedIn).await?; send.finish().await?; recv.stop(0u8.into())?; - let user_info = user_info::get_user_info(&hello.username).await?; - Ok((user_info, env)) } @@ -1302,6 +1083,7 @@ async fn handle_conn( ) -> Result<()> { info!("established"); + let mut stream_buf = Vec::new(); loop { let stream = conn.accept_bi().await; let (send, mut recv) = match stream { @@ -1315,7 +1097,7 @@ async fn handle_conn( Ok(s) => s, }; - let stream = read_msg::(&mut recv).await??; + let stream = read_msg::(&mut recv, &mut stream_buf).await??; let span = info_span!( "stream", r#type = ?stream @@ -1522,22 +1304,22 @@ async fn handle_stream_shell( extern "C" { #[cfg(any(target_os = "macos", target_os = "ios"))] - pub fn _NSGetEnviron() -> *mut *const *const std::os::raw::c_char; + pub fn _NSGetEnviron() -> *mut *const *const std::ffi::c_char; #[cfg(not(any(target_os = "macos", target_os = "ios")))] - static mut environ: *const *const c_char; + static mut environ: *const *const std::ffi::c_char; } let mut keep = Vec::new(); const PRESERVE_ENV: &[&[u8]] = &[b"PATH\0", b"LANG\0"]; unsafe { #[cfg(any(target_os = "macos", target_os = "ios"))] - let mut environ = *_NSGetEnviron(); + let mut env = *_NSGetEnviron(); #[cfg(not(any(target_os = "macos", target_os = "ios")))] - let mut environ = environ; - if !environ.is_null() { - while !(*environ).is_null() { - let key_value = CStr::from_ptr(*environ).to_bytes_with_nul(); - environ = environ.add(1); + let mut env = environ; + if !env.is_null() { + while !(*env).is_null() { + let key_value = CStr::from_ptr(*env).to_bytes_with_nul(); + env = env.add(1); let Some(i) = key_value.iter().position(|b| *b == b'=') else { continue }; @@ -1583,7 +1365,7 @@ async fn handle_stream_shell( target_os = "redox", target_os = "haiku" )))] - nix::unistd::setgroups(&user_info.groups.into_iter().map(|g| g.id).collect()) + nix::unistd::setgroups(&user_info.groups.iter().map(|g| g.id).collect::>()) .context("setting supplementary groups")?; nix::unistd::setgid(user_info.group.id).context("setting primary group")?; @@ -1737,7 +1519,7 @@ async fn handle_stream_shell( let r = pty.read_buf(buf).await; //_ = waker_recv.try_recv(); if let Err(e) = r { - if e.raw_os_error() == Some(35) { + if e.raw_os_error() == Some(35) || e.raw_os_error() == Some(11) { //debug!("not ready: {}", e); //tokio::task::yield_now().await; //tokio::time::sleep(std::time::Duration::from_millis(1)).await;