michal/tit
Browse tree · Show commit · Download archive
Diff
bb331b826e96 → eb46623bc00e
.github/workflows/ci.yml
Mode 100644 → 100644; object 76a2f673d056 → 567e41ba194b
@@ -30,5 +30,6 @@
persist-credentials: false
- run: cargo --version
- run: cargo test --locked --all-targets --all-features
+ - run: cargo test --locked --release --test auth --test ssh
- run: cargo test --locked --release --test sqlite_workload -- --ignored --nocapture
- run: cargo build --locked --release
Cargo.lock
Mode 100644 → 100644; object 4451e6686ecd → 4dd0b064da83
@@ -3,16 +3,168 @@ version = 4 [[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", + "zeroize", +] + +[[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 = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] +name = "argon2" +version = "0.6.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.11.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] [[package]] name = "cc" @@ -29,6 +181,50 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "rand_core", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", + "zeroize", +] [[package]] name = "clap" @@ -69,11 +265,243 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] name = "displaydoc" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common", + "digest", + "ff", + "group", + "hkdf", + "hybrid-array", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", "proc-macro2", "quote", "syn 2.0.119", @@ -92,7 +520,7 @@ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -114,6 +542,22 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -129,14 +573,157 @@ ] [[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-array" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +dependencies = [ + "generic-array 0.14.9", + "rustversion", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] name = "getrandom" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core", + "subtle", ] [[package]] @@ -150,6 +737,72 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "ctutils", + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[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 = "icu_collections" @@ -265,6 +918,59 @@ ] [[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "internal-russh-num-bigint" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" +dependencies = [ + "num-integer", + "num-traits", + "rand", + "rand_core", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common", + "rand_core", +] + +[[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -294,10 +1000,193 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "md5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8", + "rand_core", + "sha3 0.11.0", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "pageant" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5" +dependencies = [ + "base16ct", + "byteorder", + "bytes", + "delegate", + "futures", + "log", + "rand", + "sha2", + "thiserror", + "tokio", + "windows", + "windows-strings", +] + +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "phc", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] [[package]] name = "percent-encoding" @@ -306,10 +1195,77 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "der", + "pbkdf2", + "rand_core", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + +[[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures", + "universal-hash", + "zeroize", +] + +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures", + "universal-hash", +] [[package]] name = "potential_utf" @@ -318,6 +1274,33 @@ checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common", + "ff", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -345,6 +1328,47 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] name = "rusqlite" version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -358,6 +1382,108 @@ ] [[package]] +name = "russh" +version = "0.62.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b67b5a0d8068c89dcbe9d95df986af7a851d1f3c604525274c37468e60464f" +dependencies = [ + "aes", + "bitflags", + "block-padding", + "byteorder", + "bytes", + "cbc", + "cipher", + "crypto-bigint", + "ctr", + "curve25519-dalek", + "data-encoding", + "delegate", + "der", + "digest", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "futures", + "generic-array 1.4.4", + "getrandom 0.4.3", + "ghash", + "hex-literal", + "hmac", + "inout", + "internal-russh-num-bigint", + "keccak", + "log", + "md5", + "ml-kem", + "module-lattice", + "num-bigint", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2", + "pkcs5", + "pkcs8", + "polyval", + "rand", + "rand_core", + "ring", + "russh-cryptovec", + "russh-util", + "salsa20", + "scrypt", + "sec1", + "sha1", + "sha2", + "sha3 0.12.0", + "signature", + "spki", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror", + "tokio", + "typenum", + "universal-hash", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" +dependencies = [ + "log", + "nix", + "ssh-encoding", + "windows-sys 0.61.2", +] + +[[package]] +name = "russh-util" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" +dependencies = [ + "chrono", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -367,8 +1493,56 @@ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "salsa20" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" +dependencies = [ + "cfg-if", + "cipher", +] + +[[package]] +name = "scrypt" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" +dependencies = [ + "cfg-if", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -410,10 +1584,79 @@ ] [[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + +[[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -422,10 +1665,99 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "ssh-cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "chacha20", + "cipher", + "ctutils", + "des", + "poly1305", + "ssh-encoding", + "zeroize", +] + +[[package]] +name = "ssh-encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" +dependencies = [ + "base64ct", + "bytes", + "ctutils", + "digest", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "ssh-key" +version = "0.7.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" +dependencies = [ + "argon2", + "bcrypt-pbkdf", + "ctutils", + "ed25519-dalek", + "hex", + "hmac", + "p256", + "p384", + "p521", + "rand_core", + "sec1", + "sha1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "zeroize", +] + +[[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -467,10 +1799,10 @@ checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -508,12 +1840,43 @@ version = "0.1.0" dependencies = [ "clap", + "rand", "rusqlite", + "russh", "serde", + "sha2", + "ssh-key", "tempfile", "thiserror", + "tokio", "toml", "url", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -556,10 +1919,32 @@ checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" @@ -587,10 +1972,181 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] [[package]] name = "windows-sys" @@ -602,10 +2158,94 @@ ] [[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] [[package]] name = "writeable" @@ -656,6 +2296,12 @@ "syn 2.0.119", "synstructure", ] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie"
Cargo.toml
Mode 100644 → 100644; object 26ceb22c474a → f954d7e44109
@@ -12,9 +12,14 @@
[dependencies]
clap = { version = "4.6", default-features = false, features = ["derive", "error-context", "help", "std", "usage"] }
+rand = "0.10"
rusqlite = { version = "0.40", default-features = false, features = ["backup", "bundled"] }
+russh = { version = "0.62", default-features = false, features = ["ring"] }
serde = { version = "1.0", features = ["derive"] }
+sha2 = { version = "0.11", default-features = false }
+ssh-key = { version = "=0.7.0-rc.11", default-features = false, features = ["ed25519", "p256", "std"] }
thiserror = "2.0"
+tokio = { version = "1.53", default-features = false, features = ["macros", "net", "rt-multi-thread", "sync", "time"] }
toml = { version = "1.1", default-features = false, features = ["parse", "serde", "std"] }
url = { version = "2.5", features = ["serde"] }
README.md
Mode 100644 → 100644; object 3250c8a5c92d → df309213a0cc
@@ -38,6 +38,20 @@ [architectural decision record](docs/adr/0001-sqlite-storage.md) for the limits and current platform evidence. +## Milestone 1B gate + +Install stock OpenSSH. Then, run the SSH identity gate: + +```text +./scripts/check-m1b +``` + +This command uses stock `ssh`, `ssh-agent`, `ssh-add`, and `ssh-keygen` to do +tests of public-key authentication, SSH request restrictions, and SSHSIG login +challenges. Read the SSH identity +[architectural decision record](docs/adr/0002-ssh-identity.md) for the supported +algorithms, limits, and current platform evidence. + ## Database check An initialized instance keeps its metadata in `tit.sqlite3`. Check an existing
deny.toml
Mode 100644 → 100644; object ad1443a5358b → 7b80f0d03a76
@@ -7,6 +7,8 @@
confidence-threshold = 0.93
allow = [
"Apache-2.0",
+ "BSD-3-Clause",
+ "ISC",
"MIT",
"Unicode-3.0",
]
docs/adr/0002-ssh-identity.md
Mode → 100644; object → ecb88c54e1b9
@@ -1,0 +1,103 @@ +# Architectural decision record 0002: SSH identity + +Status: Provisional + +Date: 2026-07-22 + +## Context + +`tit` needs one SSH identity for Git transport, SSH commands, and Web login. +The SSH username must not select the account. The SSH public key must select +the account. Web login must use the standard SSHSIG format and must prevent +replay. + +## Decision + +Use `ssh-key` version 0.7.0-rc.11 for OpenSSH public-key parsing, +fingerprints, and SSHSIG verification. Russh version 0.62.4 uses this exact +version, which prevents two SSH key models in the executable. Use Russh with +the `ring` feature for the built-in SSH server. Do not enable compression or +the Russh RSA feature. Use Tokio for the asynchronous listener. Use `rand` for +operating-system random bytes and `sha2` for login nonce hashes. + +Accept Ed25519 and ECDSA P-256 public keys. Remove the public-key comment during +normalization and use its SHA-256 fingerprint. Reject DSA, other ECDSA curves, +security keys, certificates, unknown algorithms, and RSA. Report an RSA key +that is smaller than 3,072 bits as undersized before the general RSA rejection. + +Russh advertises RSA-SHA2, but its server handler does not receive the RSA +signature algorithm that the client selected. Thus, the application cannot +independently reject a custom client that uses RSA-SHA1. Do not accept RSA keys +until this boundary can enforce RSA-SHA2. A stock OpenSSH client that is forced +to use `ssh-rsa` cannot authenticate to the M1B server. + +Use this canonical login challenge: + +```text +tit-auth-v1 +purpose=web-login +origin=ORIGIN +username=USERNAME +fingerprint=FINGERPRINT +nonce=NONCE +issued-at=ISSUED_TIME +expires-at=EXPIRY_TIME +``` + +The trailing newline is necessary. Sign the exact bytes with the `tit-auth` +SSHSIG namespace. A challenge is valid for a maximum of five minutes. The +challenge is at most 4 KiB. The SSHSIG envelope and SSH public key are each at +most 16 KiB. + +Keep only the SHA-256 hash and expiry of each active login nonce. Keep a maximum +of 1,024 active challenges and remove expired entries when a new challenge is +issued. Consume the hash under one lock only after all context and signature +checks pass. A process restart invalidates all challenges in this feasibility +implementation. Milestone 3 must put nonce hashes in SQLite before Web login is +available. + +The SSH server accepts public-key authentication only. It ignores the SSH +username. It accepts session channels and the exact `tit --version` command. +It rejects shells, PTYs, subsystems, agents, forwarding, and all other exec +requests. It accepts only `GIT_PROTOCOL` environment requests with the exact +values `version=0`, `version=1`, or `version=2`. + +## Threats and controls + +A copied SSHSIG envelope cannot start a second session because the first valid +verification removes its nonce hash. Concurrent verification can have only one +successful result. A signer cannot change the origin, username, fingerprint, +issue time, or expiry because these fields are in the signed bytes. The verifier +also compares these fields with its own origin, username, selected key, clock, +and stored expiry. + +The key and signature parsers have input limits before cryptographic work. The +server has only public-key authentication, three authentication attempts, a +30-second inactivity limit, and a 32-KiB packet limit from Russh. Rejected SSH +requests do not start a shell or a process. + +## Evidence + +The local macOS gate uses stock `ssh`, `ssh-agent`, `ssh-add`, and `ssh-keygen`. +It verifies Ed25519 and ECDSA P-256 authentication with two different SSH +usernames. It verifies SSHSIG envelopes that stock `ssh-keygen -Y sign` creates. +It also verifies the negative cases for replay, concurrent use, expiry, origin, +namespace, key, malformed input, DSA, ECDSA P-384, RSA, RSA-SHA1, an unknown +key, shells, PTYs, subsystems, agents, forwarding, arbitrary exec requests, and +environment values. + +This decision stays provisional until the Linux and macOS hosted M1B gates +pass. + +## Consequences + +The selected crates add cryptographic and asynchronous code to the executable, +but they remove custom SSH cryptography and transport code. The initial key set +is smaller because Russh does not give the application the information that it +needs to enforce RSA-SHA2. This omission is safer than acceptance of RSA-SHA1. + +The server boundary is not connected to `tit serve` in M1B because the account +store and instance host-key lifecycle do not exist. The server accepts an +explicit set of normalized keys. Subsequent milestones must supply that set +through the account and authorization services without changing the transport +policy.
scripts/check-m1b
Mode → 100755; object → 6b238181687a
@@ -1,0 +1,5 @@ +#!/bin/sh +set -eu + +./scripts/check +cargo test --locked --release --test auth --test ssh
src/auth.rs
Mode → 100644; object → 51223225fc54
@@ -1,0 +1,369 @@
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use rand::TryRng;
+use sha2::{Digest, Sha256};
+use ssh_key::{Algorithm, EcdsaCurve, HashAlg, PublicKey, SshSig};
+use thiserror::Error;
+use url::Url;
+
+const CHALLENGE_HEADER: &str = "tit-auth-v1";
+const CHALLENGE_PURPOSE: &str = "web-login";
+const SIGNATURE_NAMESPACE: &str = "tit-auth";
+const MAX_KEY_BYTES: usize = 16 * 1024;
+const MAX_CHALLENGE_BYTES: usize = 4 * 1024;
+const MAX_SIGNATURE_BYTES: usize = 16 * 1024;
+const MAX_CHALLENGE_LIFETIME_SECONDS: u64 = 5 * 60;
+const MAX_OUTSTANDING_CHALLENGES: usize = 1_024;
+const NONCE_BYTES: usize = 32;
+const MINIMUM_RSA_BITS: u32 = 3_072;
+
+#[derive(Clone, Debug)]
+pub(crate) struct SshPublicKey {
+ key: PublicKey,
+ canonical: String,
+ fingerprint: String,
+}
+
+impl SshPublicKey {
+ pub(crate) fn parse(input: &str) -> Result<Self, AuthError> {
+ if input.len() > MAX_KEY_BYTES {
+ return Err(AuthError::InputTooLarge("SSH public key"));
+ }
+
+ let mut key = PublicKey::from_openssh(input).map_err(AuthError::PublicKey)?;
+ validate_key_algorithm(&key)?;
+ key.set_comment("");
+ let canonical = key.to_openssh().map_err(AuthError::PublicKey)?;
+ let fingerprint = key.fingerprint(HashAlg::Sha256).to_string();
+ Ok(Self {
+ key,
+ canonical,
+ fingerprint,
+ })
+ }
+
+ pub(crate) fn canonical(&self) -> &str {
+ &self.canonical
+ }
+
+ pub(crate) fn fingerprint(&self) -> &str {
+ &self.fingerprint
+ }
+
+ pub(crate) fn public_key(&self) -> &PublicKey {
+ &self.key
+ }
+}
+
+#[derive(Debug)]
+pub(crate) struct LoginChallenges {
+ origin: String,
+ nonces: Mutex<HashMap<[u8; 32], u64>>,
+}
+
+impl LoginChallenges {
+ pub(crate) fn new(public_url: &Url) -> Result<Self, AuthError> {
+ if !matches!(public_url.scheme(), "http" | "https")
+ || public_url.host().is_none()
+ || !public_url.username().is_empty()
+ || public_url.password().is_some()
+ || public_url.query().is_some()
+ || public_url.fragment().is_some()
+ || public_url.path() != "/"
+ {
+ return Err(AuthError::InvalidOrigin);
+ }
+
+ Ok(Self {
+ origin: public_url.origin().ascii_serialization(),
+ nonces: Mutex::new(HashMap::new()),
+ })
+ }
+
+ pub(crate) fn issue(
+ &self,
+ username: &str,
+ key: &SshPublicKey,
+ issued_at: u64,
+ lifetime_seconds: u64,
+ ) -> Result<String, AuthError> {
+ validate_username(username)?;
+ if lifetime_seconds == 0 || lifetime_seconds > MAX_CHALLENGE_LIFETIME_SECONDS {
+ return Err(AuthError::InvalidLifetime);
+ }
+ let expires_at = issued_at
+ .checked_add(lifetime_seconds)
+ .ok_or(AuthError::InvalidLifetime)?;
+
+ let nonce = self.new_nonce(issued_at, expires_at)?;
+ Ok(format!(
+ "{CHALLENGE_HEADER}\npurpose={CHALLENGE_PURPOSE}\norigin={}\nusername={username}\nfingerprint={}\nnonce={}\nissued-at={issued_at}\nexpires-at={expires_at}\n",
+ self.origin,
+ key.fingerprint(),
+ encode_hex(&nonce)
+ ))
+ }
+
+ pub(crate) fn verify(
+ &self,
+ challenge: &str,
+ signature: &str,
+ expected_username: &str,
+ expected_key: &SshPublicKey,
+ now: u64,
+ ) -> Result<VerifiedLogin, AuthError> {
+ if challenge.len() > MAX_CHALLENGE_BYTES {
+ return Err(AuthError::InputTooLarge("login challenge"));
+ }
+ if signature.len() > MAX_SIGNATURE_BYTES {
+ return Err(AuthError::InputTooLarge("SSHSIG envelope"));
+ }
+ validate_username(expected_username)?;
+
+ let fields = ChallengeFields::parse(challenge)?;
+ if fields.origin != self.origin {
+ return Err(AuthError::WrongOrigin);
+ }
+ if fields.username != expected_username {
+ return Err(AuthError::WrongUsername);
+ }
+ if fields.fingerprint != expected_key.fingerprint() {
+ return Err(AuthError::WrongKey);
+ }
+ if fields.expires_at <= fields.issued_at
+ || fields.expires_at - fields.issued_at > MAX_CHALLENGE_LIFETIME_SECONDS
+ {
+ return Err(AuthError::InvalidLifetime);
+ }
+ if now < fields.issued_at || now > fields.expires_at {
+ return Err(AuthError::ExpiredChallenge);
+ }
+
+ let sshsig = SshSig::from_pem(signature).map_err(AuthError::SignatureEnvelope)?;
+ validate_signature_algorithm(expected_key.public_key(), &sshsig)?;
+ expected_key
+ .public_key()
+ .verify(SIGNATURE_NAMESPACE, challenge.as_bytes(), &sshsig)
+ .map_err(AuthError::SignatureVerification)?;
+
+ let nonce_hash = hash_nonce(&fields.nonce);
+ let mut nonces = self.nonces.lock().map_err(|_| AuthError::NonceStore)?;
+ match nonces.get(&nonce_hash) {
+ Some(stored_expiry) if *stored_expiry == fields.expires_at => {
+ nonces.remove(&nonce_hash);
+ }
+ _ => return Err(AuthError::ConsumedChallenge),
+ }
+
+ Ok(VerifiedLogin {
+ username: fields.username.to_owned(),
+ fingerprint: fields.fingerprint.to_owned(),
+ })
+ }
+
+ fn new_nonce(&self, issued_at: u64, expires_at: u64) -> Result<[u8; NONCE_BYTES], AuthError> {
+ loop {
+ let mut nonce = [0_u8; NONCE_BYTES];
+ rand::rngs::SysRng
+ .try_fill_bytes(&mut nonce)
+ .map_err(|_| AuthError::Random)?;
+ let hash = hash_nonce(&nonce);
+ let mut nonces = self.nonces.lock().map_err(|_| AuthError::NonceStore)?;
+ nonces.retain(|_, stored_expiry| *stored_expiry >= issued_at);
+ if nonces.len() >= MAX_OUTSTANDING_CHALLENGES {
+ return Err(AuthError::NonceLimit);
+ }
+ if let std::collections::hash_map::Entry::Vacant(entry) = nonces.entry(hash) {
+ entry.insert(expires_at);
+ return Ok(nonce);
+ }
+ }
+ }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub(crate) struct VerifiedLogin {
+ pub(crate) username: String,
+ pub(crate) fingerprint: String,
+}
+
+#[derive(Debug, Error)]
+pub(crate) enum AuthError {
+ #[error("{0} is too large")]
+ InputTooLarge(&'static str),
+ #[error("SSH public key is not valid: {0}")]
+ PublicKey(ssh_key::Error),
+ #[error("SSH public key algorithm is not supported")]
+ UnsupportedKeyAlgorithm,
+ #[error("RSA key has {actual} bits, but a minimum of {minimum} bits is necessary")]
+ UndersizedRsa { actual: u32, minimum: u32 },
+ #[error("username is not valid")]
+ InvalidUsername,
+ #[error("canonical origin is not valid")]
+ InvalidOrigin,
+ #[error("challenge lifetime is not valid")]
+ InvalidLifetime,
+ #[error("cannot get random bytes")]
+ Random,
+ #[error("login challenge is not valid")]
+ MalformedChallenge,
+ #[error("login challenge has the wrong origin")]
+ WrongOrigin,
+ #[error("login challenge has the wrong username")]
+ WrongUsername,
+ #[error("login challenge has the wrong SSH key")]
+ WrongKey,
+ #[error("login challenge has expired or is not active")]
+ ExpiredChallenge,
+ #[error("SSHSIG envelope is not valid: {0}")]
+ SignatureEnvelope(ssh_key::Error),
+ #[error("SSHSIG algorithm is not supported")]
+ UnsupportedSignatureAlgorithm,
+ #[error("SSHSIG verification failed: {0}")]
+ SignatureVerification(ssh_key::Error),
+ #[error("login challenge was consumed or was not issued")]
+ ConsumedChallenge,
+ #[error("login nonce store is not available")]
+ NonceStore,
+ #[error("too many login challenges are active")]
+ NonceLimit,
+}
+
+struct ChallengeFields<'a> {
+ origin: &'a str,
+ username: &'a str,
+ fingerprint: &'a str,
+ nonce: [u8; NONCE_BYTES],
+ issued_at: u64,
+ expires_at: u64,
+}
+
+impl<'a> ChallengeFields<'a> {
+ fn parse(challenge: &'a str) -> Result<Self, AuthError> {
+ let body = challenge
+ .strip_suffix('\n')
+ .ok_or(AuthError::MalformedChallenge)?;
+ let mut lines = body.split('\n');
+ if lines.next() != Some(CHALLENGE_HEADER) || lines.next() != Some("purpose=web-login") {
+ return Err(AuthError::MalformedChallenge);
+ }
+ let origin = field(lines.next(), "origin=")?;
+ let username = field(lines.next(), "username=")?;
+ validate_username(username)?;
+ let fingerprint = field(lines.next(), "fingerprint=")?;
+ let nonce = decode_hex(field(lines.next(), "nonce=")?)?;
+ let issued_at = parse_time(field(lines.next(), "issued-at=")?)?;
+ let expires_at = parse_time(field(lines.next(), "expires-at=")?)?;
+ if lines.next().is_some()
+ || origin.is_empty()
+ || fingerprint.is_empty()
+ || origin.contains(['\r', '\n'])
+ || fingerprint.contains(['\r', '\n'])
+ {
+ return Err(AuthError::MalformedChallenge);
+ }
+ Ok(Self {
+ origin,
+ username,
+ fingerprint,
+ nonce,
+ issued_at,
+ expires_at,
+ })
+ }
+}
+
+fn validate_key_algorithm(key: &PublicKey) -> Result<(), AuthError> {
+ match key.key_data() {
+ ssh_key::public::KeyData::Ed25519(_) => Ok(()),
+ ssh_key::public::KeyData::Ecdsa(key) if key.curve() == EcdsaCurve::NistP256 => Ok(()),
+ ssh_key::public::KeyData::Rsa(key) if key.key_size() < MINIMUM_RSA_BITS => {
+ Err(AuthError::UndersizedRsa {
+ actual: key.key_size(),
+ minimum: MINIMUM_RSA_BITS,
+ })
+ }
+ _ => Err(AuthError::UnsupportedKeyAlgorithm),
+ }
+}
+
+fn validate_signature_algorithm(key: &PublicKey, signature: &SshSig) -> Result<(), AuthError> {
+ let valid = match (key.key_data(), signature.algorithm()) {
+ (ssh_key::public::KeyData::Ed25519(_), Algorithm::Ed25519) => true,
+ (ssh_key::public::KeyData::Ecdsa(key), Algorithm::Ecdsa { curve }) => {
+ key.curve() == EcdsaCurve::NistP256 && curve == EcdsaCurve::NistP256
+ }
+ _ => false,
+ };
+ if valid {
+ Ok(())
+ } else {
+ Err(AuthError::UnsupportedSignatureAlgorithm)
+ }
+}
+
+fn validate_username(username: &str) -> Result<(), AuthError> {
+ let bytes = username.as_bytes();
+ let valid_character = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
+ if !(1..=40).contains(&bytes.len())
+ || !valid_character(bytes[0])
+ || !valid_character(bytes[bytes.len() - 1])
+ || !bytes
+ .iter()
+ .all(|byte| valid_character(*byte) || *byte == b'-')
+ {
+ return Err(AuthError::InvalidUsername);
+ }
+ Ok(())
+}
+
+fn field<'a>(line: Option<&'a str>, prefix: &str) -> Result<&'a str, AuthError> {
+ line.and_then(|line| line.strip_prefix(prefix))
+ .ok_or(AuthError::MalformedChallenge)
+}
+
+fn parse_time(value: &str) -> Result<u64, AuthError> {
+ if value.is_empty()
+ || !value.bytes().all(|byte| byte.is_ascii_digit())
+ || (value.len() > 1 && value.starts_with('0'))
+ {
+ return Err(AuthError::MalformedChallenge);
+ }
+ value
+ .parse::<u64>()
+ .map_err(|_| AuthError::MalformedChallenge)
+}
+
+fn hash_nonce(nonce: &[u8; NONCE_BYTES]) -> [u8; 32] {
+ Sha256::digest(nonce).into()
+}
+
+fn encode_hex(bytes: &[u8]) -> String {
+ const HEX: &[u8; 16] = b"0123456789abcdef";
+ let mut encoded = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ encoded.push(char::from(HEX[usize::from(byte >> 4)]));
+ encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
+ }
+ encoded
+}
+
+fn decode_hex(value: &str) -> Result<[u8; NONCE_BYTES], AuthError> {
+ if value.len() != NONCE_BYTES * 2 {
+ return Err(AuthError::MalformedChallenge);
+ }
+ let mut bytes = [0_u8; NONCE_BYTES];
+ for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
+ bytes[index] = (decode_hex_digit(pair[0])? << 4) | decode_hex_digit(pair[1])?;
+ }
+ Ok(bytes)
+}
+
+fn decode_hex_digit(value: u8) -> Result<u8, AuthError> {
+ match value {
+ b'0'..=b'9' => Ok(value - b'0'),
+ b'a'..=b'f' => Ok(value - b'a' + 10),
+ _ => Err(AuthError::MalformedChallenge),
+ }
+}
src/main.rs
Mode 100644 → 100644; object 1a3f95b4687a → 4ae77ffedf38
@@ -1,5 +1,9 @@ +#[allow(dead_code, reason = "M1B proves SSH identity before M2 calls it")] +mod auth; mod cli; mod config; +#[allow(dead_code, reason = "M1B proves the SSH server before M2 calls it")] +mod ssh; mod store; use std::process::ExitCode;
src/ssh.rs
Mode → 100644; object → f3c93da72da3
@@ -1,0 +1,375 @@
+use std::borrow::Cow;
+use std::collections::HashSet;
+use std::net::SocketAddr;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::time::Duration;
+
+use rand::rng;
+use russh::server::{Auth, ChannelOpenHandle, Handler, Msg, Server, Session};
+use russh::{Channel, ChannelId, MethodKind, MethodSet, Preferred, Pty};
+use ssh_key::{Algorithm, EcdsaCurve, PrivateKey, PublicKey};
+use thiserror::Error;
+use tokio::net::TcpListener;
+use tokio::sync::oneshot;
+use tokio::task::JoinHandle;
+
+use crate::auth::SshPublicKey;
+
+const VERSION_COMMAND: &[u8] = b"tit --version";
+const GIT_PROTOCOL_VARIABLE: &str = "GIT_PROTOCOL";
+
+pub(crate) struct RunningSshServer {
+ address: SocketAddr,
+ handle: russh::server::RunningServerHandle,
+ task: JoinHandle<std::io::Result<()>>,
+ audit: Arc<RequestAudit>,
+}
+
+impl RunningSshServer {
+ pub(crate) async fn start(
+ address: SocketAddr,
+ authorized_keys: &[SshPublicKey],
+ ) -> Result<Self, SshServerError> {
+ let listener = TcpListener::bind(address).await?;
+ let address = listener.local_addr()?;
+ let host_key = PrivateKey::random(&mut rng(), Algorithm::Ed25519)?;
+ let mut methods = MethodSet::empty();
+ methods.push(MethodKind::PublicKey);
+ let config = Arc::new(russh::server::Config {
+ methods,
+ auth_rejection_time: Duration::from_millis(250),
+ auth_rejection_time_initial: Some(Duration::ZERO),
+ keys: vec![host_key],
+ preferred: Preferred {
+ key: Cow::Owned(vec![
+ Algorithm::Ed25519,
+ Algorithm::Ecdsa {
+ curve: EcdsaCurve::NistP256,
+ },
+ ]),
+ ..Preferred::default()
+ },
+ max_auth_attempts: 3,
+ inactivity_timeout: Some(Duration::from_secs(30)),
+ nodelay: true,
+ ..Default::default()
+ });
+ let authorized_keys = Arc::new(
+ authorized_keys
+ .iter()
+ .map(|key| key.public_key().clone())
+ .collect(),
+ );
+ let audit = Arc::new(RequestAudit::default());
+ let server = SshServer {
+ authorized_keys,
+ audit: Arc::clone(&audit),
+ };
+ let (handle_sender, handle_receiver) = oneshot::channel();
+ let task = tokio::spawn(async move {
+ let mut server = server;
+ let running = server.run_on_socket(config, &listener);
+ let _ = handle_sender.send(running.handle());
+ running.await
+ });
+ let handle = handle_receiver.await.map_err(|_| SshServerError::Startup)?;
+ Ok(Self {
+ address,
+ handle,
+ task,
+ audit,
+ })
+ }
+
+ pub(crate) fn address(&self) -> SocketAddr {
+ self.address
+ }
+
+ pub(crate) fn audit(&self) -> RequestAuditSnapshot {
+ self.audit.snapshot()
+ }
+
+ pub(crate) async fn shutdown(self) -> Result<(), SshServerError> {
+ self.handle.shutdown("tit test shutdown".to_owned());
+ self.task.await.map_err(|_| SshServerError::Join)??;
+ Ok(())
+ }
+}
+
+#[derive(Debug, Error)]
+pub(crate) enum SshServerError {
+ #[error("SSH listener error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("SSH key error: {0}")]
+ Key(#[from] ssh_key::Error),
+ #[error("SSH server did not start")]
+ Startup,
+ #[error("SSH server task failed")]
+ Join,
+}
+
+#[derive(Clone)]
+struct SshServer {
+ authorized_keys: Arc<HashSet<PublicKey>>,
+ audit: Arc<RequestAudit>,
+}
+
+impl Server for SshServer {
+ type Handler = SshSession;
+
+ fn new_client(&mut self, _peer_address: Option<SocketAddr>) -> Self::Handler {
+ SshSession {
+ authorized_keys: Arc::clone(&self.authorized_keys),
+ audit: Arc::clone(&self.audit),
+ }
+ }
+}
+
+struct SshSession {
+ authorized_keys: Arc<HashSet<PublicKey>>,
+ audit: Arc<RequestAudit>,
+}
+
+impl Handler for SshSession {
+ type Error = russh::Error;
+
+ async fn auth_publickey_offered(
+ &mut self,
+ _user: &str,
+ public_key: &PublicKey,
+ ) -> Result<Auth, Self::Error> {
+ Ok(self.authorize(public_key))
+ }
+
+ async fn auth_publickey(
+ &mut self,
+ _user: &str,
+ public_key: &PublicKey,
+ ) -> Result<Auth, Self::Error> {
+ Ok(self.authorize(public_key))
+ }
+
+ async fn channel_open_session(
+ &mut self,
+ _channel: Channel<Msg>,
+ reply: ChannelOpenHandle,
+ _session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ reply.accept().await;
+ Ok(())
+ }
+
+ async fn channel_open_x11(
+ &mut self,
+ _channel: Channel<Msg>,
+ _originator_address: &str,
+ _originator_port: u32,
+ _reply: ChannelOpenHandle,
+ _session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_forward.fetch_add(1, Ordering::Relaxed);
+ Ok(())
+ }
+
+ async fn channel_open_direct_tcpip(
+ &mut self,
+ _channel: Channel<Msg>,
+ _host_to_connect: &str,
+ _port_to_connect: u32,
+ _originator_address: &str,
+ _originator_port: u32,
+ _reply: ChannelOpenHandle,
+ _session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_forward.fetch_add(1, Ordering::Relaxed);
+ Ok(())
+ }
+
+ async fn channel_open_direct_streamlocal(
+ &mut self,
+ _channel: Channel<Msg>,
+ _socket_path: &str,
+ _reply: ChannelOpenHandle,
+ _session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_forward.fetch_add(1, Ordering::Relaxed);
+ Ok(())
+ }
+
+ async fn pty_request(
+ &mut self,
+ channel: ChannelId,
+ _term: &str,
+ _col_width: u32,
+ _row_height: u32,
+ _pix_width: u32,
+ _pix_height: u32,
+ _modes: &[(Pty, u32)],
+ session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_pty.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ Ok(())
+ }
+
+ async fn x11_request(
+ &mut self,
+ channel: ChannelId,
+ _single_connection: bool,
+ _x11_auth_protocol: &str,
+ _x11_auth_cookie: &str,
+ _x11_screen_number: u32,
+ session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_forward.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ Ok(())
+ }
+
+ async fn env_request(
+ &mut self,
+ channel: ChannelId,
+ variable_name: &str,
+ variable_value: &str,
+ session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ if variable_name == GIT_PROTOCOL_VARIABLE && valid_git_protocol(variable_value) {
+ self.audit.accepted_env.fetch_add(1, Ordering::Relaxed);
+ session.channel_success(channel)?;
+ } else {
+ self.audit.rejected_env.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ }
+ Ok(())
+ }
+
+ async fn shell_request(
+ &mut self,
+ channel: ChannelId,
+ session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_shell.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ session.close(channel)?;
+ Ok(())
+ }
+
+ async fn exec_request(
+ &mut self,
+ channel: ChannelId,
+ command: &[u8],
+ session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ if command == VERSION_COMMAND {
+ self.audit.accepted_exec.fetch_add(1, Ordering::Relaxed);
+ session.channel_success(channel)?;
+ session.data(
+ channel,
+ format!("tit {}\n", env!("CARGO_PKG_VERSION")).into_bytes(),
+ )?;
+ session.exit_status_request(channel, 0)?;
+ session.eof(channel)?;
+ session.close(channel)?;
+ } else {
+ self.audit.rejected_exec.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ session.close(channel)?;
+ }
+ Ok(())
+ }
+
+ async fn subsystem_request(
+ &mut self,
+ channel: ChannelId,
+ _name: &str,
+ session: &mut Session,
+ ) -> Result<(), Self::Error> {
+ self.audit.rejected_exec.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ session.close(channel)?;
+ Ok(())
+ }
+
+ async fn agent_request(
+ &mut self,
+ channel: ChannelId,
+ session: &mut Session,
+ ) -> Result<bool, Self::Error> {
+ self.audit.rejected_agent.fetch_add(1, Ordering::Relaxed);
+ session.channel_failure(channel)?;
+ Ok(false)
+ }
+
+ async fn tcpip_forward(
+ &mut self,
+ _address: &str,
+ _port: &mut u32,
+ _session: &mut Session,
+ ) -> Result<bool, Self::Error> {
+ self.audit.rejected_forward.fetch_add(1, Ordering::Relaxed);
+ Ok(false)
+ }
+
+ async fn streamlocal_forward(
+ &mut self,
+ _socket_path: &str,
+ _session: &mut Session,
+ ) -> Result<bool, Self::Error> {
+ self.audit.rejected_forward.fetch_add(1, Ordering::Relaxed);
+ Ok(false)
+ }
+}
+
+impl SshSession {
+ fn authorize(&self, public_key: &PublicKey) -> Auth {
+ if self.authorized_keys.contains(public_key) {
+ Auth::Accept
+ } else {
+ Auth::reject()
+ }
+ }
+}
+
+fn valid_git_protocol(value: &str) -> bool {
+ matches!(value, "version=0" | "version=1" | "version=2")
+}
+
+#[derive(Default)]
+struct RequestAudit {
+ accepted_env: AtomicUsize,
+ rejected_env: AtomicUsize,
+ accepted_exec: AtomicUsize,
+ rejected_exec: AtomicUsize,
+ rejected_shell: AtomicUsize,
+ rejected_pty: AtomicUsize,
+ rejected_agent: AtomicUsize,
+ rejected_forward: AtomicUsize,
+}
+
+impl RequestAudit {
+ fn snapshot(&self) -> RequestAuditSnapshot {
+ RequestAuditSnapshot {
+ accepted_env: self.accepted_env.load(Ordering::Relaxed),
+ rejected_env: self.rejected_env.load(Ordering::Relaxed),
+ accepted_exec: self.accepted_exec.load(Ordering::Relaxed),
+ rejected_exec: self.rejected_exec.load(Ordering::Relaxed),
+ rejected_shell: self.rejected_shell.load(Ordering::Relaxed),
+ rejected_pty: self.rejected_pty.load(Ordering::Relaxed),
+ rejected_agent: self.rejected_agent.load(Ordering::Relaxed),
+ rejected_forward: self.rejected_forward.load(Ordering::Relaxed),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default)]
+pub(crate) struct RequestAuditSnapshot {
+ pub(crate) accepted_env: usize,
+ pub(crate) rejected_env: usize,
+ pub(crate) accepted_exec: usize,
+ pub(crate) rejected_exec: usize,
+ pub(crate) rejected_shell: usize,
+ pub(crate) rejected_pty: usize,
+ pub(crate) rejected_agent: usize,
+ pub(crate) rejected_forward: usize,
+}
tests/auth.rs
Mode → 100644; object → 7dba0784dc1f
@@ -1,0 +1,461 @@
+#[path = "../src/auth.rs"]
+mod auth;
+
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::sync::{Arc, Barrier};
+use std::thread;
+
+use auth::{AuthError, LoginChallenges, SshPublicKey};
+use tempfile::TempDir;
+use url::Url;
+
+const ISSUED_AT: u64 = 1_750_000_000;
+const LIFETIME: u64 = 120;
+const DSA_PUBLIC_KEY: &str = "ssh-dss AAAAB3NzaC1kc3MAAACBANw9iSUO2UYhFMssjUgW46URqv8bBrDgHeF8HLBOWBvKuXF2Rx2J/XyhgX48SOLMuv0hcPaejlyLarabnF9F2V4dkpPpZSJ+7luHmxEjNxwhsdtg8UteXAWkeCzrQ6MvRJZHcDBjYh56KGvslbFnJsGLXlI4PQCyl6awNImwYGilAAAAFQCJGBU3hZf+QtP9Jh/nbfNlhFu7hwAAAIBHObOQioQVRm3HsVb7mOy3FVKhcLoLO3qoG9gTkd4KeuehtFAC3+rckiX7xSCnE/5BBKdL7VP9WRXac2Nlr9Pwl3e7zPut96wrCHt/TZX6vkfXKkbpUIj5zSqfvyNrWKaYJkfzwAQwrXNS1Hol676Ud/DDEn2oatdEhkS3beWHXAAAAIBgQqaz/YYTRMshzMzYcZ4lqgvgmA55y6v0h39e8HH2A5dwNS6sPUw2jyna+le0dceNRJifFld1J+WYM0vmquSr11DDavgEidOSaXwfMvPPPJqLmbzdtT16N+Gij9U9STQTHPQcQ3xnNNHgQAStzZJbhLOVbDDDo5BO7LMUALDfSA==";
+
+#[test]
+fn normalizes_supported_openssh_keys_and_matches_stock_fingerprints() {
+ let directory = TempDir::new().expect("create a key directory");
+ for fixture in [KeyFixture::Ed25519, KeyFixture::EcdsaP256] {
+ let private_key = directory.path().join(fixture.name());
+ generate_key(&private_key, fixture);
+ let public_key_path = public_key_path(&private_key);
+ let encoded = fs::read_to_string(&public_key_path).expect("read the public key");
+ let key = SshPublicKey::parse(&encoded).expect("normalize the public key");
+
+ assert_eq!(key.canonical().split_whitespace().count(), 2);
+ assert_eq!(key.fingerprint(), stock_fingerprint(&public_key_path));
+ assert_eq!(
+ SshPublicKey::parse(key.canonical())
+ .expect("parse the normalized key")
+ .canonical(),
+ key.canonical()
+ );
+ }
+}
+
+#[test]
+fn rejects_unsupported_and_undersized_keys() {
+ let directory = TempDir::new().expect("create a key directory");
+
+ assert!(matches!(
+ SshPublicKey::parse(DSA_PUBLIC_KEY),
+ Err(AuthError::UnsupportedKeyAlgorithm)
+ ));
+
+ let ecdsa_p384 = directory.path().join("ecdsa-p384");
+ generate_key(&ecdsa_p384, KeyFixture::EcdsaP384);
+ assert!(matches!(
+ parse_public_key(&ecdsa_p384),
+ Err(AuthError::UnsupportedKeyAlgorithm)
+ ));
+
+ let rsa_2048 = directory.path().join("rsa-2048");
+ generate_key(&rsa_2048, KeyFixture::Rsa2048);
+ assert!(matches!(
+ parse_public_key(&rsa_2048),
+ Err(AuthError::UndersizedRsa {
+ actual: 2_048,
+ minimum: 3_072
+ })
+ ));
+
+ let rsa_3072 = directory.path().join("rsa-3072");
+ generate_key(&rsa_3072, KeyFixture::Rsa3072);
+ assert!(matches!(
+ parse_public_key(&rsa_3072),
+ Err(AuthError::UnsupportedKeyAlgorithm)
+ ));
+
+ assert!(SshPublicKey::parse("not a key").is_err());
+}
+
+#[test]
+fn verifies_stock_sshsig_envelopes_for_each_supported_key() {
+ let directory = TempDir::new().expect("create a key directory");
+ for fixture in [KeyFixture::Ed25519, KeyFixture::EcdsaP256] {
+ let private_key = directory.path().join(fixture.name());
+ generate_key(&private_key, fixture);
+ let key = parse_public_key(&private_key).expect("parse the public key");
+ let challenges = issuer("https://tit.example/");
+ let challenge = challenges
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue a login challenge");
+ let signature = sign(
+ &directory,
+ fixture.name(),
+ &private_key,
+ "tit-auth",
+ &challenge,
+ );
+
+ let verified = challenges
+ .verify(&challenge, &signature, "alice", &key, ISSUED_AT + 1)
+ .expect("verify the stock SSHSIG envelope");
+ assert_eq!(verified.username, "alice");
+ assert_eq!(verified.fingerprint, key.fingerprint());
+ }
+}
+
+#[test]
+fn rejects_replay_expiry_wrong_context_and_malformed_envelopes() {
+ let directory = TempDir::new().expect("create a key directory");
+ let private_key = directory.path().join("ed25519");
+ generate_key(&private_key, KeyFixture::Ed25519);
+ let key = parse_public_key(&private_key).expect("parse the public key");
+
+ let replay_issuer = issuer("https://tit.example/");
+ let replay_challenge = replay_issuer
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue a replay challenge");
+ let replay_signature = sign(
+ &directory,
+ "replay",
+ &private_key,
+ "tit-auth",
+ &replay_challenge,
+ );
+ replay_issuer
+ .verify(
+ &replay_challenge,
+ &replay_signature,
+ "alice",
+ &key,
+ ISSUED_AT + 1,
+ )
+ .expect("verify the challenge one time");
+ assert!(matches!(
+ replay_issuer.verify(
+ &replay_challenge,
+ &replay_signature,
+ "alice",
+ &key,
+ ISSUED_AT + 1
+ ),
+ Err(AuthError::ConsumedChallenge)
+ ));
+
+ let expired_issuer = issuer("https://tit.example/");
+ let expired_challenge = expired_issuer
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue an expiring challenge");
+ let expired_signature = sign(
+ &directory,
+ "expired",
+ &private_key,
+ "tit-auth",
+ &expired_challenge,
+ );
+ assert!(matches!(
+ expired_issuer.verify(
+ &expired_challenge,
+ &expired_signature,
+ "alice",
+ &key,
+ ISSUED_AT + LIFETIME + 1
+ ),
+ Err(AuthError::ExpiredChallenge)
+ ));
+
+ let namespace_issuer = issuer("https://tit.example/");
+ let namespace_challenge = namespace_issuer
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue a namespace challenge");
+ let wrong_namespace = sign(
+ &directory,
+ "namespace",
+ &private_key,
+ "other",
+ &namespace_challenge,
+ );
+ assert!(matches!(
+ namespace_issuer.verify(
+ &namespace_challenge,
+ &wrong_namespace,
+ "alice",
+ &key,
+ ISSUED_AT + 1
+ ),
+ Err(AuthError::SignatureVerification(_))
+ ));
+
+ let origin_issuer = issuer("https://tit.example/");
+ let origin_challenge = origin_issuer
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue an origin challenge");
+ let origin_signature = sign(
+ &directory,
+ "origin",
+ &private_key,
+ "tit-auth",
+ &origin_challenge,
+ );
+ assert!(matches!(
+ issuer("https://other.example/").verify(
+ &origin_challenge,
+ &origin_signature,
+ "alice",
+ &key,
+ ISSUED_AT + 1
+ ),
+ Err(AuthError::WrongOrigin)
+ ));
+
+ let malformed_issuer = issuer("https://tit.example/");
+ let malformed_challenge = malformed_issuer
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue a malformed-envelope challenge");
+ assert!(matches!(
+ malformed_issuer.verify(
+ &malformed_challenge,
+ "not an SSHSIG envelope",
+ "alice",
+ &key,
+ ISSUED_AT + 1
+ ),
+ Err(AuthError::SignatureEnvelope(_))
+ ));
+}
+
+#[test]
+fn rejects_wrong_key_username_and_rsa_keys() {
+ let directory = TempDir::new().expect("create a key directory");
+ let first_private = directory.path().join("first");
+ let second_private = directory.path().join("second");
+ generate_key(&first_private, KeyFixture::Ed25519);
+ generate_key(&second_private, KeyFixture::Ed25519);
+ let first_key = parse_public_key(&first_private).expect("parse the first public key");
+ let second_key = parse_public_key(&second_private).expect("parse the second public key");
+ let challenges = issuer("https://tit.example/");
+ let challenge = challenges
+ .issue("alice", &first_key, ISSUED_AT, LIFETIME)
+ .expect("issue a key challenge");
+ let signature = sign(
+ &directory,
+ "wrong-key",
+ &first_private,
+ "tit-auth",
+ &challenge,
+ );
+
+ assert!(matches!(
+ challenges.verify(&challenge, &signature, "alice", &second_key, ISSUED_AT + 1),
+ Err(AuthError::WrongKey)
+ ));
+ assert!(matches!(
+ challenges.verify(&challenge, &signature, "bob", &first_key, ISSUED_AT + 1),
+ Err(AuthError::WrongUsername)
+ ));
+
+ let rsa_private = directory.path().join("rsa");
+ generate_key(&rsa_private, KeyFixture::Rsa3072);
+ assert!(matches!(
+ parse_public_key(&rsa_private),
+ Err(AuthError::UnsupportedKeyAlgorithm)
+ ));
+}
+
+#[test]
+fn consumes_a_nonce_atomically() {
+ let directory = TempDir::new().expect("create a key directory");
+ let private_key = directory.path().join("ed25519");
+ generate_key(&private_key, KeyFixture::Ed25519);
+ let key = parse_public_key(&private_key).expect("parse the public key");
+ let challenges = Arc::new(issuer("https://tit.example/"));
+ let challenge = challenges
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue a concurrent challenge");
+ let signature = sign(
+ &directory,
+ "concurrent",
+ &private_key,
+ "tit-auth",
+ &challenge,
+ );
+ let barrier = Arc::new(Barrier::new(8));
+
+ let workers: Vec<_> = (0..8)
+ .map(|_| {
+ let challenges = Arc::clone(&challenges);
+ let barrier = Arc::clone(&barrier);
+ let challenge = challenge.clone();
+ let signature = signature.clone();
+ let key = key.clone();
+ thread::spawn(move || {
+ barrier.wait();
+ challenges
+ .verify(&challenge, &signature, "alice", &key, ISSUED_AT + 1)
+ .is_ok()
+ })
+ })
+ .collect();
+
+ let successes = workers
+ .into_iter()
+ .map(|worker| worker.join().expect("join a verifier"))
+ .filter(|success| *success)
+ .count();
+ assert_eq!(successes, 1);
+}
+
+#[test]
+fn enforces_input_and_active_challenge_limits() {
+ let directory = TempDir::new().expect("create a key directory");
+ let private_key = directory.path().join("ed25519");
+ generate_key(&private_key, KeyFixture::Ed25519);
+ let key = parse_public_key(&private_key).expect("parse the public key");
+
+ assert!(matches!(
+ SshPublicKey::parse(&"x".repeat(16 * 1024 + 1)),
+ Err(AuthError::InputTooLarge("SSH public key"))
+ ));
+
+ let challenges = issuer("https://tit.example/");
+ let challenge = challenges
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("issue a size-limit challenge");
+ assert!(matches!(
+ challenges.verify(
+ &"x".repeat(4 * 1024 + 1),
+ "signature",
+ "alice",
+ &key,
+ ISSUED_AT + 1
+ ),
+ Err(AuthError::InputTooLarge("login challenge"))
+ ));
+ assert!(matches!(
+ challenges.verify(
+ &challenge,
+ &"x".repeat(16 * 1024 + 1),
+ "alice",
+ &key,
+ ISSUED_AT + 1
+ ),
+ Err(AuthError::InputTooLarge("SSHSIG envelope"))
+ ));
+
+ let bounded = issuer("https://tit.example/");
+ for _ in 0..1_024 {
+ bounded
+ .issue("alice", &key, ISSUED_AT, LIFETIME)
+ .expect("fill the active challenge store");
+ }
+ assert!(matches!(
+ bounded.issue("alice", &key, ISSUED_AT, LIFETIME),
+ Err(AuthError::NonceLimit)
+ ));
+ bounded
+ .issue("alice", &key, ISSUED_AT + LIFETIME + 1, LIFETIME)
+ .expect("prune expired challenges before issuing another one");
+}
+
+fn issuer(origin: &str) -> LoginChallenges {
+ LoginChallenges::new(&Url::parse(origin).expect("parse the test origin"))
+ .expect("create a login challenge issuer")
+}
+
+fn parse_public_key(private_key: &Path) -> Result<SshPublicKey, AuthError> {
+ let encoded = fs::read_to_string(public_key_path(private_key)).expect("read the public key");
+ SshPublicKey::parse(&encoded)
+}
+
+fn public_key_path(private_key: &Path) -> PathBuf {
+ let mut path = private_key.as_os_str().to_owned();
+ path.push(".pub");
+ PathBuf::from(path)
+}
+
+fn stock_fingerprint(public_key: &Path) -> String {
+ let output = Command::new("ssh-keygen")
+ .args(["-l", "-E", "sha256", "-f"])
+ .arg(public_key)
+ .output()
+ .expect("run ssh-keygen fingerprint");
+ assert!(output.status.success());
+ String::from_utf8(output.stdout)
+ .expect("read the fingerprint output")
+ .split_whitespace()
+ .nth(1)
+ .expect("find the fingerprint")
+ .to_owned()
+}
+
+fn sign(
+ directory: &TempDir,
+ name: &str,
+ private_key: &Path,
+ namespace: &str,
+ message: &str,
+) -> String {
+ let message_path = directory.path().join(format!("{name}.challenge"));
+ fs::write(&message_path, message).expect("write the challenge");
+ let output = Command::new("ssh-keygen")
+ .args(["-q", "-Y", "sign", "-f"])
+ .arg(private_key)
+ .args(["-n", namespace])
+ .arg(&message_path)
+ .output()
+ .expect("run ssh-keygen sign");
+ assert!(
+ output.status.success(),
+ "sign the challenge: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ let mut signature_path = message_path.into_os_string();
+ signature_path.push(".sig");
+ fs::read_to_string(signature_path).expect("read the SSHSIG envelope")
+}
+
+#[derive(Clone, Copy)]
+enum KeyFixture {
+ Ed25519,
+ EcdsaP256,
+ EcdsaP384,
+ Rsa2048,
+ Rsa3072,
+}
+
+impl KeyFixture {
+ fn name(self) -> &'static str {
+ match self {
+ Self::Ed25519 => "ed25519",
+ Self::EcdsaP256 => "ecdsa-p256",
+ Self::EcdsaP384 => "ecdsa-p384",
+ Self::Rsa2048 => "rsa-2048",
+ Self::Rsa3072 => "rsa-3072",
+ }
+ }
+}
+
+fn generate_key(path: &Path, fixture: KeyFixture) {
+ let mut command = Command::new("ssh-keygen");
+ command.args(["-q", "-N", "", "-C", "test comment", "-f"]);
+ command.arg(path);
+ match fixture {
+ KeyFixture::Ed25519 => {
+ command.args(["-t", "ed25519"]);
+ }
+ KeyFixture::EcdsaP256 => {
+ command.args(["-t", "ecdsa", "-b", "256"]);
+ }
+ KeyFixture::EcdsaP384 => {
+ command.args(["-t", "ecdsa", "-b", "384"]);
+ }
+ KeyFixture::Rsa2048 => {
+ command.args(["-t", "rsa", "-b", "2048"]);
+ }
+ KeyFixture::Rsa3072 => {
+ command.args(["-t", "rsa", "-b", "3072"]);
+ }
+ }
+ let output = command.output().expect("run ssh-keygen key generation");
+ assert!(
+ output.status.success(),
+ "generate {}: {}",
+ fixture.name(),
+ String::from_utf8_lossy(&output.stderr)
+ );
+}
tests/ssh.rs
Mode → 100644; object → 92555510b96d
@@ -1,0 +1,311 @@
+#[allow(dead_code, reason = "the SSH test uses only the shared key boundary")]
+#[path = "../src/auth.rs"]
+mod auth;
+#[path = "../src/ssh.rs"]
+mod ssh;
+
+use std::fs;
+use std::net::{Ipv4Addr, SocketAddr};
+use std::path::{Path, PathBuf};
+use std::process::{Child, Command, Output, Stdio};
+use std::thread;
+use std::time::{Duration, Instant};
+
+use auth::SshPublicKey;
+use ssh::RunningSshServer;
+use tempfile::TempDir;
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn stock_openssh_authenticates_supported_keys_and_ignores_the_username() {
+ for fixture in [KeyFixture::Ed25519, KeyFixture::EcdsaP256] {
+ let directory = TempDir::new().expect("create a key directory");
+ let private_key = directory.path().join(fixture.name());
+ generate_key(&private_key, fixture);
+ let key = parse_public_key(&private_key);
+ let server = start(&[key]).await;
+
+ for username in ["alice", "the-ssh-user-is-ignored"] {
+ let output = ssh(&server, &private_key, username, &["tit --version"]);
+ assert!(
+ output.status.success(),
+ "authenticate {}: {}",
+ fixture.name(),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert_eq!(
+ String::from_utf8(output.stdout).expect("read the version output"),
+ format!("tit {}\n", env!("CARGO_PKG_VERSION"))
+ );
+ }
+
+ assert_eq!(server.audit().accepted_exec, 2);
+ server.shutdown().await.expect("stop the SSH server");
+ }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn rejects_unknown_keys_and_forced_rsa_sha1_authentication() {
+ let directory = TempDir::new().expect("create a key directory");
+ let authorized_private = directory.path().join("authorized");
+ let unknown_private = directory.path().join("unknown");
+ generate_key(&authorized_private, KeyFixture::Ed25519);
+ generate_key(&unknown_private, KeyFixture::Ed25519);
+ let authorized_key = parse_public_key(&authorized_private);
+ let server = start(&[authorized_key]).await;
+
+ let unknown = ssh(&server, &unknown_private, "alice", &["tit --version"]);
+ assert!(!unknown.status.success());
+ server.shutdown().await.expect("stop the SSH server");
+
+ let rsa_private = directory.path().join("rsa");
+ generate_key(&rsa_private, KeyFixture::Rsa3072);
+ let rsa_server = start(&[parse_public_key(&authorized_private)]).await;
+ let rsa_sha1 = ssh(
+ &rsa_server,
+ &rsa_private,
+ "alice",
+ &["-o", "PubkeyAcceptedAlgorithms=ssh-rsa", "tit --version"],
+ );
+ assert!(
+ !rsa_sha1.status.success(),
+ "the server accepted RSA-SHA1: {}",
+ String::from_utf8_lossy(&rsa_sha1.stdout)
+ );
+ rsa_server
+ .shutdown()
+ .await
+ .expect("stop the RSA SSH server");
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn rejects_shell_pty_arbitrary_exec_subsystem_agent_and_forwarding() {
+ let directory = TempDir::new().expect("create a key directory");
+ let private_key = directory.path().join("ed25519");
+ generate_key(&private_key, KeyFixture::Ed25519);
+ let key = parse_public_key(&private_key);
+ let server = start(&[key]).await;
+
+ assert!(!ssh(&server, &private_key, "alice", &[]).status.success());
+ assert!(
+ !ssh(&server, &private_key, "alice", &["uname", "-a"])
+ .status
+ .success()
+ );
+ assert!(
+ !ssh(&server, &private_key, "alice", &["-s", "sftp"])
+ .status
+ .success()
+ );
+ let pty = ssh(&server, &private_key, "alice", &["-tt", "tit --version"]);
+ assert!(
+ !pty.status.success() || !pty.stderr.is_empty(),
+ "the PTY request was not rejected"
+ );
+ assert!(
+ !ssh(&server, &private_key, "alice", &["-W", "127.0.0.1:1"])
+ .status
+ .success()
+ );
+ let agent_socket = directory.path().join("agent.sock");
+ let mut agent_process = start_agent(&agent_socket, &private_key);
+ let agent = ssh_with_env(
+ &server,
+ &private_key,
+ "alice",
+ &["-A", "tit --version"],
+ &[(
+ "SSH_AUTH_SOCK",
+ agent_socket.to_str().expect("a UTF-8 agent socket path"),
+ )],
+ );
+ agent_process.kill().expect("stop the test SSH agent");
+ agent_process.wait().expect("wait for the test SSH agent");
+ assert!(agent.status.success());
+
+ let audit = server.audit();
+ assert!(audit.rejected_shell >= 1);
+ assert!(audit.rejected_exec >= 2);
+ assert!(audit.rejected_pty >= 1);
+ assert!(audit.rejected_agent >= 1);
+ assert!(audit.rejected_forward >= 1);
+ server.shutdown().await.expect("stop the SSH server");
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn accepts_only_strict_git_protocol_environment_values() {
+ let directory = TempDir::new().expect("create a key directory");
+ let private_key = directory.path().join("ed25519");
+ generate_key(&private_key, KeyFixture::Ed25519);
+ let key = parse_public_key(&private_key);
+ let server = start(&[key]).await;
+
+ for value in ["version=0", "version=1", "version=2"] {
+ let output = ssh_with_env(
+ &server,
+ &private_key,
+ "alice",
+ &["-o", "SendEnv=GIT_PROTOCOL", "tit --version"],
+ &[("GIT_PROTOCOL", value)],
+ );
+ assert!(output.status.success());
+ }
+ for value in ["", "2", "version=3", "version=2:extra", "version=2\nBAD=1"] {
+ let output = ssh_with_env(
+ &server,
+ &private_key,
+ "alice",
+ &["-o", "SendEnv=GIT_PROTOCOL", "tit --version"],
+ &[("GIT_PROTOCOL", value)],
+ );
+ assert!(output.status.success());
+ }
+ let unrelated = ssh_with_env(
+ &server,
+ &private_key,
+ "alice",
+ &["-o", "SendEnv=UNRELATED", "tit --version"],
+ &[("UNRELATED", "value")],
+ );
+ assert!(unrelated.status.success());
+
+ let audit = server.audit();
+ assert_eq!(audit.accepted_env, 3);
+ assert_eq!(audit.rejected_env, 6);
+ server.shutdown().await.expect("stop the SSH server");
+}
+
+async fn start(keys: &[SshPublicKey]) -> RunningSshServer {
+ RunningSshServer::start(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), keys)
+ .await
+ .expect("start the SSH server")
+}
+
+fn ssh(
+ server: &RunningSshServer,
+ private_key: &Path,
+ username: &str,
+ arguments: &[&str],
+) -> Output {
+ ssh_with_env(server, private_key, username, arguments, &[])
+}
+
+fn ssh_with_env(
+ server: &RunningSshServer,
+ private_key: &Path,
+ username: &str,
+ arguments: &[&str],
+ environment: &[(&str, &str)],
+) -> Output {
+ let mut command = Command::new("ssh");
+ command.args([
+ "-F",
+ "/dev/null",
+ "-o",
+ "BatchMode=yes",
+ "-o",
+ "IdentitiesOnly=yes",
+ "-o",
+ "StrictHostKeyChecking=no",
+ "-o",
+ "UserKnownHostsFile=/dev/null",
+ "-o",
+ "LogLevel=ERROR",
+ "-o",
+ "PreferredAuthentications=publickey",
+ "-o",
+ "PasswordAuthentication=no",
+ "-o",
+ "KbdInteractiveAuthentication=no",
+ "-o",
+ "ConnectTimeout=5",
+ "-i",
+ ]);
+ command.arg(private_key);
+ command.args(["-p", &server.address().port().to_string()]);
+ command.arg(format!("{username}@{}", server.address().ip()));
+ command.args(arguments);
+ command.envs(environment.iter().copied());
+ command.output().expect("run the stock SSH client")
+}
+
+fn parse_public_key(private_key: &Path) -> SshPublicKey {
+ let encoded = fs::read_to_string(public_key_path(private_key)).expect("read the public key");
+ SshPublicKey::parse(&encoded).expect("parse the public key")
+}
+
+fn public_key_path(private_key: &Path) -> PathBuf {
+ let mut path = private_key.as_os_str().to_owned();
+ path.push(".pub");
+ PathBuf::from(path)
+}
+
+#[derive(Clone, Copy)]
+enum KeyFixture {
+ Ed25519,
+ EcdsaP256,
+ Rsa3072,
+}
+
+impl KeyFixture {
+ fn name(self) -> &'static str {
+ match self {
+ Self::Ed25519 => "ed25519",
+ Self::EcdsaP256 => "ecdsa-p256",
+ Self::Rsa3072 => "rsa-3072",
+ }
+ }
+}
+
+fn generate_key(path: &Path, fixture: KeyFixture) {
+ let mut command = Command::new("ssh-keygen");
+ command.args(["-q", "-N", "", "-f"]);
+ command.arg(path);
+ match fixture {
+ KeyFixture::Ed25519 => {
+ command.args(["-t", "ed25519"]);
+ }
+ KeyFixture::EcdsaP256 => {
+ command.args(["-t", "ecdsa", "-b", "256"]);
+ }
+ KeyFixture::Rsa3072 => {
+ command.args(["-t", "rsa", "-b", "3072"]);
+ }
+ }
+ let output = command.output().expect("run ssh-keygen key generation");
+ assert!(
+ output.status.success(),
+ "generate {}: {}",
+ fixture.name(),
+ String::from_utf8_lossy(&output.stderr)
+ );
+}
+
+fn start_agent(socket: &Path, private_key: &Path) -> Child {
+ let mut child = Command::new("ssh-agent")
+ .args(["-D", "-a"])
+ .arg(socket)
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .spawn()
+ .expect("start the stock SSH agent");
+ let deadline = Instant::now() + Duration::from_secs(5);
+ while !socket.exists() {
+ if let Some(status) = child.try_wait().expect("inspect the SSH agent") {
+ panic!("SSH agent stopped before it was ready: {status}");
+ }
+ assert!(Instant::now() < deadline, "SSH agent was not ready");
+ thread::sleep(Duration::from_millis(10));
+ }
+
+ let output = Command::new("ssh-add")
+ .arg(private_key)
+ .env("SSH_AUTH_SOCK", socket)
+ .output()
+ .expect("add the test key to the SSH agent");
+ assert!(
+ output.status.success(),
+ "add the test key to the SSH agent: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ child
+}