commit ac9681e0c30874650b6fd69966098c52b4bd4a82 Author: Dustin C. Hatch Date: Sat Nov 4 13:52:16 2023 -0500 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6d39a8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[**.rs] +max_line_length = 79 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..383338e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Cargo.lock -diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d701e9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/config.toml +/machine_ids.json +/host_ca_key* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..811308e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2058 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "argh" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" +dependencies = [ + "serde", +] + +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", +] + +[[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", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form-data-builder" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff8fb4527b05539a9f573ba2831a1127038a7b45eea385a338a63dc5ab6829" +dependencies = [ + "base64 0.13.1", + "rand", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.5", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.5", + "ring", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "version_check", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "platforms" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primeorder" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rsa" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2180b3bc4955efd5661a97658d3cf4c8107e0d132f619195afe9486c13cca313" +dependencies = [ + "ed25519-dalek", + "p256", + "p384", + "rand_core", + "rsa", + "sec1", + "serde", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "sshca" +version = "0.1.0" +dependencies = [ + "argh", + "argon2", + "axum", + "dirs", + "form-data-builder", + "hyper", + "jsonwebtoken", + "rand_core", + "serde", + "serde_json", + "serial_test", + "ssh-key", + "tempfile", + "tokio", + "toml", + "tower", + "tracing", + "tracing-subscriber", + "uuid", + "virt", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bca379b1d06d60012e5d335bd839b26c2aa84bdbd358e4c02a6e151cf73db81" +dependencies = [ + "libc", + "virt-sys", +] + +[[package]] +name = "virt-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd39f6e0d0ab3fe7c371fac05c9b6ca72e186f06a2e666fb3b95441091eba2db" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b49f5fa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "sshca" +version = "0.1.0" +edition = "2021" + +[dependencies] +argh = "0.1.12" +argon2 = { version = "0.5.2", default-features = false, features = ["alloc"] } +axum = { version = "0.6.20", features = ["multipart", "headers", "json"] } +dirs = "5.0.1" +jsonwebtoken = { version = "8.3.0", default-features = false } +rand_core = { version = "0.6.4", features = ["getrandom"] } +serde = { version = "1.0.190", features = ["derive"] } +serde_json = "1.0.108" +ssh-key = { version = "0.6.2", features = ["serde", "ed25519", "getrandom"] } +tokio = { version = "1.33.0", features = ["rt", "macros", "net", "signal", "fs", "io-util"] } +toml = "0.8.6" +tracing = { version = "0.1.40", features = ["log"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +uuid = "1.5.0" +virt = { version = "0.3.1", optional = true } + +[features] +default = ["libvirt"] +libvirt = ["dep:virt"] + +[dev-dependencies] +form-data-builder = "1.0.1" +hyper = "0.14.27" +serial_test = "2.0.0" +tempfile = "3.8.1" +tower = { version = "0.4.13", features = ["util"] } + diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..372048c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 79 diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..aac685b --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,136 @@ +//! Authentication +//! +//! SSHCA authentication is handled using JWTs. Each request must include a +//! proper token, with the target system's hostname as the subject and issuer, +//! signed by that system's machine ID. +//! +//! To identify identify the subject of a token, use [`get_token_subject`]. +//! This function will return the hostname specified in the token; use this +//! to look up the machine ID of the system. Then, use [`validate_token`] to +//! validate the token. + +use argon2::Argon2; +use jsonwebtoken::errors::Result; +use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; +use rand_core::{OsRng, RngCore}; +use serde::Deserialize; +use tracing::error; +use uuid::Uuid; + +/// JWT Token Claims +#[derive(Debug, Deserialize)] +pub struct Claims { + /// Token subject (machine hostname) + pub sub: String, +} + +/// Extract the subject from the token +/// +/// This function takes a JWT string and returns the value of the `sub` claim. +/// The signature of the token is not validated, but if the token has expired, +/// an error is returned. +pub fn get_token_subject(token: &str) -> Result { + let mut v = Validation::new(Algorithm::HS256); + v.insecure_disable_signature_validation(); + v.set_required_spec_claims(&["sub"]); + let k = DecodingKey::from_secret(b""); + let data: TokenData = decode(token, &k, &v)?; + Ok(data.claims.sub) +} + +/// Validate a token +/// +/// This function validates a JWT for the specified hostname and its +/// corresponding machine ID. The token is valid if it is issued for the +/// specified hostname by the same, has an audience matching the value of the +/// `service` argument, and is within its validity period (not before/expires). +/// The token must be signed with HMAC-SHA256 using the host's machine ID as +/// the secret key. +pub fn validate_token( + token: &str, + hostname: &str, + machine_id: &Uuid, + service: &str, +) -> Result { + let mut v = Validation::new(Algorithm::HS256); + v.validate_nbf = true; + v.set_issuer(&[hostname]); + v.set_audience(&[service]); + let mut secret = [0u8; 32]; + if let Err(e) = Argon2::default().hash_password_into( + machine_id.as_bytes(), + hostname.as_bytes(), + &mut secret, + ) { + error!("Could not derive token secret: {}", e); + OsRng.fill_bytes(&mut secret); + } + let k = DecodingKey::from_secret(&secret); + let data: TokenData = decode(token, &k, &v)?; + Ok(data.claims) +} + +#[cfg(test)] +pub(crate) mod test { + use std::time; + + use jsonwebtoken::{encode, EncodingKey}; + use serde::Serialize; + use uuid::uuid; + + use super::*; + + #[derive(Debug, Serialize)] + struct TestClaims { + sub: String, + iss: String, + aud: String, + iat: u64, + nbf: u64, + exp: u64, + } + + pub(crate) fn make_token(hostname: &str, machine_id: Uuid) -> String { + let now = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let claims = TestClaims { + sub: hostname.into(), + iss: hostname.into(), + aud: "sshca.example.org".into(), + nbf: now - 60, + iat: now, + exp: now + 60, + }; + let mut secret = [0u8; 32]; + Argon2::default() + .hash_password_into( + machine_id.as_bytes(), + hostname.as_bytes(), + &mut secret, + ) + .unwrap(); + let key = EncodingKey::from_secret(&secret); + encode(&Default::default(), &claims, &key).unwrap() + } + + #[test] + fn test_get_token_subject() { + let hostname = "web0.example.org"; + let machine_id = uuid!("890ec6fe-e6c2-4524-bec0-d474af8aa506"); + let token = make_token(hostname, machine_id); + let sub = get_token_subject(&token).unwrap(); + assert_eq!(sub, hostname); + } + + #[test] + fn test_validate_token() { + let hostname = "file0.example.org"; + + let machine_id = uuid!("9afd42e5-4ac3-4530-90c4-191869063ef9"); + let token = make_token(hostname, machine_id); + validate_token(&token, hostname, &machine_id, "sshca.example.org") + .unwrap(); + } +} diff --git a/src/ca.rs b/src/ca.rs new file mode 100644 index 0000000..9a8a39b --- /dev/null +++ b/src/ca.rs @@ -0,0 +1,171 @@ +//! SSH certificate authority +use std::path::Path; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use ssh_key::certificate::{Builder, CertType}; +use ssh_key::rand_core::OsRng; +use ssh_key::{Certificate, PrivateKey, PublicKey}; +use tokio::fs::File; +use tokio::io::AsyncReadExt; +use tracing::debug; + +#[derive(Debug)] +pub enum CertError { + SystemTime(std::time::SystemTimeError), + SshKey(ssh_key::Error), +} + +impl From for CertError { + fn from(e: std::time::SystemTimeError) -> Self { + Self::SystemTime(e) + } +} + +impl From for CertError { + fn from(e: ssh_key::Error) -> Self { + Self::SshKey(e) + } +} + +impl std::fmt::Display for CertError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::SystemTime(e) => write!(f, "Invalid time: {}", e), + Self::SshKey(e) => write!(f, "SSH key error: {}", e), + } + } +} + +impl std::error::Error for CertError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::SystemTime(e) => Some(e), + Self::SshKey(e) => Some(e), + } + } +} + +#[derive(Debug)] +pub enum LoadKeyError { + Utf8(std::str::Utf8Error), + Io(std::io::Error), + SshKey(ssh_key::Error), +} + +impl From for LoadKeyError { + fn from(e: std::str::Utf8Error) -> Self { + Self::Utf8(e) + } +} + +impl From for LoadKeyError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +impl From for LoadKeyError { + fn from(e: ssh_key::Error) -> Self { + Self::SshKey(e) + } +} + +impl std::fmt::Display for LoadKeyError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Utf8(e) => write!(f, "Invalid key: {}", e), + Self::Io(e) => write!(f, "Could not read key file: {}", e), + Self::SshKey(e) => write!(f, "Could not parse SSH key: {}", e), + } + } +} + +impl std::error::Error for LoadKeyError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Utf8(e) => Some(e), + Self::Io(e) => Some(e), + Self::SshKey(e) => Some(e), + } + } +} + +/// Load an SSH private key from a file +pub async fn load_private_key

(path: P) -> Result +where + P: AsRef, +{ + let mut data = Vec::new(); + debug!("Loading private key from {}", path.as_ref().display()); + let mut f = File::open(path).await?; + f.read_to_end(&mut data).await?; + parse_private_key(&data) +} + +/// Parse an SSH private key from a slice of bytes +pub fn parse_private_key(data: &[u8]) -> Result { + Ok(PrivateKey::from_openssh(std::str::from_utf8(data)?)?) +} + +/// Parse an SSH public key from a slice of bytes +pub fn parse_public_key(data: &[u8]) -> Result { + Ok(PublicKey::from_openssh(std::str::from_utf8(data)?)?) +} + +/// Create a signed SSH certificate for a host public key +/// +/// This function creates a signed certificate for an SSH host public +/// key. The certificate will be valid for the specified hostname and +/// any alias names provided. +pub fn sign_cert( + hostname: &str, + pubkey: &PublicKey, + duration: Duration, + privkey: &PrivateKey, + alias: &[&str], +) -> Result { + let now = SystemTime::now(); + let not_before = now.duration_since(UNIX_EPOCH)?.as_secs(); + let not_after = not_before + duration.as_secs(); + + let mut builder = Builder::new_with_random_nonce( + &mut OsRng, pubkey, not_before, not_after, + )?; + builder.cert_type(CertType::Host)?; + builder.valid_principal(hostname)?; + for a in alias { + builder.valid_principal(*a)?; + } + + Ok(builder.sign(privkey)?) +} + +#[cfg(test)] +mod test { + use ssh_key::Algorithm; + + use super::*; + + #[test] + fn test_sign_cert() { + let ca_key = + PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let host_key = + PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let host_pub_key = host_key.public_key(); + let duration = Duration::from_secs(86400 * 30); + let hostname = "cloud0.example.org"; + let cert = sign_cert( + hostname, + &host_pub_key, + duration, + &ca_key, + &["nextcloud.example.org"], + ) + .unwrap(); + + let valid_principals = cert.valid_principals(); + assert_eq!(valid_principals[0], "cloud0.example.org"); + assert_eq!(valid_principals[1], "nextcloud.example.org"); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f80417c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,211 @@ +//! Application configuration +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +use serde::Deserialize; +use tracing::debug; + +/// Error returned by [`load_config`] +#[derive(Debug)] +pub enum ConfigError { + Io(std::io::Error), + Toml(toml::de::Error), +} + +impl From for ConfigError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +impl From for ConfigError { + fn from(e: toml::de::Error) -> Self { + Self::Toml(e) + } +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "Could not read config file: {}", e), + Self::Toml(e) => write!(f, "Could not parse config: {}", e), + } + } +} + +impl std::error::Error for ConfigError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(e) => Some(e), + Self::Toml(e) => Some(e), + } + } +} + +/// Host CA Configuration +#[derive(Debug, Deserialize)] +pub struct HostCaConfig { + /// Path to the Host CA private key file + #[serde(default = "default_host_ca_key")] + pub private_key_file: PathBuf, + + /// Duration of issued host certificates + #[serde(default = "default_host_cert_duration")] + pub cert_duration: u64, +} + +impl Default for HostCaConfig { + fn default() -> Self { + Self { + private_key_file: default_host_ca_key(), + cert_duration: default_host_cert_duration(), + } + } +} + +/// CA configuration +#[derive(Debug, Default, Deserialize)] +pub struct CaConfig { + /// Host CA configuration + pub host: HostCaConfig, +} + +/// Defines a connection to a libvirt VM host +#[derive(Debug, Deserialize)] +pub struct LibvirtConfig { + /// libvirt Connection URI + pub uri: String, +} + +/// Top-level configuration structure +#[derive(Debug, Deserialize)] +pub struct Configuration { + /// List of libvirt connection options + #[serde(default)] + pub libvirt: Vec, + /// Path to the machine ID map JSON file + #[serde(default = "default_machine_ids")] + pub machine_ids: PathBuf, + /// CA configuration + #[serde(default)] + pub ca: CaConfig, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + libvirt: vec![], + machine_ids: default_machine_ids(), + ca: Default::default(), + } + } +} + +fn default_config_path(basename: &str) -> PathBuf { + dirs::config_dir().map_or(PathBuf::from(basename), |mut p| { + p.push(env!("CARGO_PKG_NAME")); + p.push(basename); + p + }) +} + +fn default_machine_ids() -> PathBuf { + default_config_path("machine_ids.json") +} + +fn default_host_ca_key() -> PathBuf { + default_config_path("host-ca.key") +} + +fn default_host_cert_duration() -> u64 { + 86400 * 30 +} + +/// Load configuration from a TOML file +/// +/// If `path` is provided, the configuration will be loaded from the +/// TOML file at that location. If `path` is `None`, the path will be +/// inferred from the XDG Configuration directory (i.e. +/// `${XDG_CONFIG_HOME}/sshca/config.toml`). +/// +/// If the configuration file does not exist, the default values will be +/// used. If any error is encountered while reading or parsing the +/// file, a [`ConfigError`] will be returned. +pub fn load_config

(path: Option

) -> Result +where + P: AsRef, +{ + let path = match path { + Some(p) => PathBuf::from(p.as_ref()), + None => default_config_path("config.toml"), + }; + debug!("Loading configuration from {}", path.display()); + match std::fs::read_to_string(path) { + Ok(s) => Ok(toml::from_str(&s)?), + Err(ref e) if e.kind() == ErrorKind::NotFound => { + Ok(Default::default()) + } + Err(e) => Err(e.into()), + } +} + +#[cfg(test)] +mod test { + use serial_test::serial; + + use super::*; + + #[test] + #[serial] + fn test_default_config() { + std::env::remove_var("XDG_CONFIG_HOME"); + std::env::set_var("HOME", "/home/user"); + let config = Configuration::default(); + assert_eq!( + config.machine_ids, + PathBuf::from("/home/user/.config/sshca/machine_ids.json"), + ); + assert_eq!(config.libvirt.len(), 0); + } + + #[test] + #[serial] + fn test_default_config_path() { + std::env::remove_var("XDG_CONFIG_HOME"); + std::env::set_var("HOME", "/home/user"); + let path = default_config_path("config.toml"); + assert_eq!( + path, + PathBuf::from("/home/user/.config/sshca/config.toml"), + ); + } + + #[test] + #[serial] + fn test_default_config_path_env() { + std::env::set_var("XDG_CONFIG_HOME", "/etc"); + let path = default_config_path("config.toml"); + assert_eq!(path, PathBuf::from("/etc/sshca/config.toml")); + } + + #[test] + fn test_config_toml() { + let config_toml = r#" +[[libvirt]] +uri = "qemu+ssh://vmhost0.example.org/system" + +[[libvirt]] +uri = "qemu+ssh://vmhost1.example.org/system" +"#; + let config: Configuration = toml::from_str(config_toml).unwrap(); + assert_eq!(config.libvirt.len(), 2); + assert_eq!( + config.libvirt[0].uri, + "qemu+ssh://vmhost0.example.org/system" + ); + assert_eq!( + config.libvirt[1].uri, + "qemu+ssh://vmhost1.example.org/system" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..82d4f5e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +mod auth; +mod ca; +pub mod config; +#[cfg(feature = "libvirt")] +mod libvirt; +mod machine_id; +pub mod server; diff --git a/src/libvirt.rs b/src/libvirt.rs new file mode 100644 index 0000000..8a44878 --- /dev/null +++ b/src/libvirt.rs @@ -0,0 +1,53 @@ +//! libvirt VM info +use tracing::error; +use virt::connect::Connect; +use virt::domain::Domain; + +use crate::config::LibvirtConfig; + +/// libvirt connection wrapper with auto-disconnect +/// +/// This structure wraps a virConnect object and automatically calls +/// virConnectClose when it is dropped. +struct Connection { + conn: Connect, +} + +impl Drop for Connection { + fn drop(&mut self) { + if let Err(e) = self.conn.close() { + error!("Failed to close libvirt connection: {}", e); + } + } +} + +/// Get the UUID of a libvirt VM +/// +/// This function connects to the libvirt daemon specified in the provided +/// [`LibvirtConfig`] structure and attempts to get the UUID of the virtual +/// machine with the given name. If the connection cannot be established, or +/// no machine is found with that name, an error is returned. +pub fn get_machine_id( + name: &str, + config: &LibvirtConfig, +) -> Result { + let conn = Connection { + conn: Connect::open_read_only(&config.uri)?, + }; + let dom = Domain::lookup_by_name(&conn.conn, name)?; + dom.get_uuid_string() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_machine_id() { + let config = LibvirtConfig { + uri: "test:///default".into(), + }; + let machine_id = get_machine_id("test", &config).unwrap(); + assert_eq!(&machine_id, "6695eb01-f6a4-8304-79aa-97f2502e193f"); + } +} diff --git a/src/machine_id.rs b/src/machine_id.rs new file mode 100644 index 0000000..dd88af9 --- /dev/null +++ b/src/machine_id.rs @@ -0,0 +1,108 @@ +//! Look up a known machine ID +use std::sync::Arc; + +use serde_json::Value; +use tokio::task; +use tracing::{debug, error}; +use uuid::Uuid; + +use crate::config::Configuration; +use crate::libvirt; + +/// Look up the machine ID for a host +/// +/// This function will return the machine ID for a known host, either from the +/// static map stored in a JSON file or from a libvirt VM host. If no location +/// has a record for the machine, `None` is returned. +pub async fn get_machine_id( + hostname: &str, + config: Arc, +) -> Option { + if let Some(v) = from_map(hostname, config.clone()).await { + return parse_uuid(&v); + } + #[cfg(feature = "libvirt")] + if let Some(v) = from_libvirt(hostname, config.clone()).await { + return parse_uuid(&v); + } + None +} + +/// Look up a machine ID in the JSON map file +/// +/// This function reads the machine ID map stored in the JSON file. If the +/// file could not be opened, is not a valid JSON object, or does not contain +/// an entry for the specified hostname, `None` is returned. +async fn from_map( + hostname: &str, + config: Arc, +) -> Option { + let data = match tokio::fs::read_to_string(&config.machine_ids).await { + Ok(d) => d, + Err(e) => { + error!("Failed to read machine ID map file: {}", e); + return None; + } + }; + let res = + task::spawn_blocking(move || serde_json::from_str::(&data)) + .await; + let map = match res { + Ok(Ok(r)) => r, + Ok(Err(e)) => { + error!("Error parsing machine ID map: {}", e); + return None; + } + Err(e) => { + error!("Unexpected error while parsing JSON: {}", e); + return None; + } + }; + Some(map.as_object()?.get(hostname)?.as_str()?.to_owned()) +} + +/// Look up a machine ID on the configured libvirt VM hosts +/// +/// This function iterates over the configured VM hosts and checks each one for +/// a domain matching the specified hostname. If no VM host is available or +/// none of the configured hosts have a matching domain, `None` is returned. +#[cfg(feature = "libvirt")] +async fn from_libvirt( + hostname: &str, + config: Arc, +) -> Option { + let hostname = Arc::new(hostname.split('.').next().unwrap().to_string()); + let res = task::spawn_blocking(move || { + for libvirt in &config.libvirt { + debug!("Checking {} for {}", libvirt.uri, hostname); + match libvirt::get_machine_id(&hostname, libvirt) { + Ok(v) => return Some(v), + Err(e) => { + debug!("libvirt error: {}", e); + } + }; + } + None + }) + .await; + match res { + Ok(v) => v, + Err(e) => { + error!("Unexpected error while querying libvirt: {}", e); + None + } + } +} + +/// Parse a UUID from a string +/// +/// Returns `None` if the string does not contain a valid UUID. +fn parse_uuid(value: &str) -> Option { + Uuid::parse_str(value).map_or_else( + |e| { + error!("Invalid UUID: {}", e); + None + }, + Some, + ) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e74c000 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,46 @@ +use std::path::PathBuf; + +use argh::FromArgs; +use tracing_subscriber::EnvFilter; + +use sshca::config; +use sshca::server; + +/// Online SSH CA Service +#[derive(FromArgs)] +struct Args { + /// path to configuration file + #[argh(option, short = 'c')] + config_file: Option, + + /// listen address + #[argh(option, short = 'l')] + listen_address: Option, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_writer(std::io::stderr) + .init(); + + // stop libvirt printing to stderr + virt::error::clear_error_callback(); + + let args: Args = argh::from_env(); + + let listen_address = args + .listen_address + .as_ref() + .unwrap_or(&"[::]:8087".into()) + .parse() + .unwrap(); + + let config = config::load_config(args.config_file.as_deref()).unwrap(); + let app = server::make_app(config); + axum::Server::bind(&listen_address) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/src/server/host.rs b/src/server/host.rs new file mode 100644 index 0000000..5bfd42e --- /dev/null +++ b/src/server/host.rs @@ -0,0 +1,147 @@ +use std::collections::HashMap; +use std::time::Duration; + +use axum::extract::multipart::{Multipart, MultipartError}; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use serde::Serialize; +use ssh_key::Algorithm; +use tracing::{debug, error, info, warn}; + +use crate::auth::Claims; +use crate::ca; + +#[derive(Serialize)] +pub struct SignKeyResponse { + success: bool, + errors: Vec, + certificates: HashMap, +} + +pub enum SignKeyError { + Multipart(MultipartError), + Cert(ca::CertError), + LoadPrivateKey(ca::LoadKeyError), + ParsePublicKey(ca::LoadKeyError), + UnsupportedAlgorithm(String), + NoKey, +} + +impl From for SignKeyError { + fn from(e: MultipartError) -> Self { + Self::Multipart(e) + } +} + +impl From for SignKeyError { + fn from(e: ca::CertError) -> Self { + Self::Cert(e) + } +} + +impl IntoResponse for SignKeyError { + fn into_response(self) -> Response { + match self { + Self::Multipart(e) => { + debug!("Error reading request: {}", e); + let body = e.to_string(); + (StatusCode::BAD_REQUEST, body).into_response() + } + Self::Cert(e) => { + error!("Failed to sign certificate: {}", e); + let body = "Service Unavailable"; + (StatusCode::SERVICE_UNAVAILABLE, body).into_response() + } + Self::LoadPrivateKey(e) => { + error!("Error loading CA private key: {}", e); + let body = "Service Unavailable"; + (StatusCode::SERVICE_UNAVAILABLE, body).into_response() + } + Self::ParsePublicKey(e) => { + error!("Error parsing public keykey: {}", e); + let body = e.to_string(); + (StatusCode::BAD_REQUEST, body).into_response() + } + Self::UnsupportedAlgorithm(a) => { + debug!("Requested certificate for unsupported key algorithm \"{}\"", a); + let body = format!("Unsupported key algorithm: {}", a); + (StatusCode::BAD_REQUEST, body).into_response() + } + Self::NoKey => { + debug!("No SSH public key provided in request"); + ( + StatusCode::BAD_REQUEST, + "No SSH public key provided in request", + ) + .into_response() + } + } + } +} + +#[derive(Default)] +struct SignKeyRequest { + hostname: String, + pubkey: Vec, + aliases: Vec, +} + +pub(super) async fn sign_host_cert( + claims: Claims, + State(ctx): State, + mut form: Multipart, +) -> Result { + let hostname = claims.sub; + let mut body = SignKeyRequest::default(); + + while let Some(field) = form.next_field().await? { + match field.name() { + Some("pubkey") => { + body.pubkey = field.bytes().await?.into(); + } + Some("alias") => body.aliases.push(field.text().await?), + Some("hostname") => body.hostname = field.text().await?, + Some(n) => { + warn!("Client request included unsupported field {:?}", n); + } + None => {} + } + } + if body.pubkey.is_empty() { + return Err(SignKeyError::NoKey); + } + let aliases: Vec<_> = body.aliases.iter().map(String::as_ref).collect(); + + let config = &ctx.config; + let duration = Duration::from_secs(config.ca.host.cert_duration); + let privkey = ca::load_private_key(&config.ca.host.private_key_file) + .await + .map_err(SignKeyError::LoadPrivateKey)?; + + let pubkey = ca::parse_public_key(&body.pubkey) + .map_err(SignKeyError::ParsePublicKey)?; + match pubkey.algorithm() { + Algorithm::Ecdsa { .. } => (), + Algorithm::Ed25519 => (), + Algorithm::Rsa { .. } => (), + _ => { + return Err(SignKeyError::UnsupportedAlgorithm( + pubkey.algorithm().as_str().into(), + )); + } + }; + debug!( + "Signing {} key for {}", + pubkey.algorithm().as_str(), + hostname + ); + let cert = + ca::sign_cert(&hostname, &pubkey, duration, &privkey, &aliases)?; + info!( + "Signed {} key for {}", + pubkey.algorithm().as_str(), + hostname + ); + Ok(cert.to_openssh().map_err(ca::CertError::from)?) +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..b5146e6 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,110 @@ +mod host; + +use std::sync::Arc; + +use axum::async_trait; +use axum::extract::FromRequestParts; +use axum::headers::authorization::Bearer; +use axum::headers::{Authorization, Host}; +use axum::http::request::Parts; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{RequestPartsExt, Router, TypedHeader}; +use tracing::debug; + +use crate::auth::{self, Claims}; +use crate::config::Configuration; +use crate::machine_id; + +struct Context { + config: Arc, +} + +type State = Arc; + +pub struct AuthError; + +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() + } +} + +#[async_trait] +impl FromRequestParts> for Claims { + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut Parts, + ctx: &State, + ) -> Result { + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|e| { + debug!("Failed to extract token from HTTP request: {}", e); + AuthError + })?; + let host = parts.extract::>().await.map_or_else( + |_| "localhost".to_owned(), + |v| v.hostname().to_owned(), + ); + + let hostname = + auth::get_token_subject(bearer.token()).map_err(|e| { + debug!("Could not get token subject: {}", e); + AuthError + })?; + let machine_id = + machine_id::get_machine_id(&hostname, ctx.config.clone()) + .await + .ok_or_else(|| { + debug!("No machine ID found for host {}", hostname); + AuthError + })?; + let claims = auth::validate_token( + bearer.token(), + &hostname, + &machine_id, + &host, + ) + .map_err(|e| { + debug!("Invalid auth token: {}", e); + AuthError + })?; + debug!("Successfully authenticated request from host {}", hostname); + Ok(claims) + } +} + +pub fn make_app(config: Configuration) -> Router { + let ctx = Arc::new(Context { + config: config.into(), + }); + Router::new() + .route("/", get(|| async { "UP" })) + .route("/host/sign", post(host::sign_host_cert)) + .with_state(ctx) +} + +#[cfg(test)] +mod test { + use axum::body::Body; + use axum::http::Request; + use tower::ServiceExt; + + use super::*; + + #[tokio::test] + async fn test_up() { + let app = make_app(Configuration::default()); + let response = app + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"UP"); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..ed22a40 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,2 @@ +pub mod setup; +pub mod token; diff --git a/tests/common/setup.rs b/tests/common/setup.rs new file mode 100644 index 0000000..cb587f1 --- /dev/null +++ b/tests/common/setup.rs @@ -0,0 +1,71 @@ +use std::error::Error; +use std::io::prelude::*; +use std::path::Path; +use std::sync::Once; + +use rand_core::OsRng; +use ssh_key::{Algorithm, Fingerprint, PrivateKey, PublicKey}; +use tempfile::NamedTempFile; +use tracing_subscriber::EnvFilter; + +use sshca::config::Configuration; + +static INIT: Once = Once::new(); + +fn gen_machine_ids() -> Result> { + let f = NamedTempFile::new()?; + let map = serde_json::json!({ + "test.example.org": "b75e9126-d73a-4ae0-9a0d-63cb3552e6cd", + }); + serde_json::to_writer(&f, &map)?; + Ok(f) +} + +fn gen_config(machine_ids: &Path, host_key: &Path) -> Configuration { + let mut config = Configuration { + machine_ids: machine_ids.to_str().unwrap().into(), + ..Default::default() + }; + config.ca.host.private_key_file = host_key.to_str().unwrap().into(); + config +} + +fn gen_ca_key() -> Result<(NamedTempFile, PublicKey), Box> { + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519)?; + let mut f = NamedTempFile::new()?; + f.write_all(key.to_openssh(Default::default())?.as_bytes())?; + Ok((f, key.public_key().clone())) +} + +pub async fn setup() -> Result<(TestContext, Configuration), Box> { + INIT.call_once(|| { + tracing_subscriber::fmt::fmt() + .with_env_filter(EnvFilter::from("sshca=trace")) + .with_test_writer() + .init(); + }); + + let machine_ids = gen_machine_ids()?; + let (host_key, host_key_pub) = gen_ca_key()?; + let config = gen_config(machine_ids.path(), host_key.path()); + + let ctx = TestContext { + machine_ids, + host_key, + host_key_pub, + }; + Ok((ctx, config)) +} + +#[allow(dead_code)] +pub struct TestContext { + machine_ids: NamedTempFile, + host_key: NamedTempFile, + host_key_pub: PublicKey, +} + +impl TestContext { + pub fn host_ca_fingerprint(&self) -> Fingerprint { + self.host_key_pub.fingerprint(Default::default()) + } +} diff --git a/tests/common/token.rs b/tests/common/token.rs new file mode 100644 index 0000000..246b1f3 --- /dev/null +++ b/tests/common/token.rs @@ -0,0 +1,41 @@ +use std::time; + +use argon2::Argon2; +use jsonwebtoken::{encode, EncodingKey}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Debug, Serialize)] +struct TestClaims { + sub: String, + iss: String, + aud: String, + iat: u64, + nbf: u64, + exp: u64, +} + +pub fn make_token(hostname: &str, machine_id: Uuid) -> String { + let now = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let claims = TestClaims { + sub: hostname.into(), + iss: hostname.into(), + aud: "sshca.example.org".into(), + nbf: now - 60, + iat: now, + exp: now + 60, + }; + let mut secret = [0u8; 32]; + Argon2::default() + .hash_password_into( + machine_id.as_bytes(), + hostname.as_bytes(), + &mut secret, + ) + .unwrap(); + let key = EncodingKey::from_secret(&secret); + encode(&Default::default(), &claims, &key).unwrap() +} diff --git a/tests/test_host.rs b/tests/test_host.rs new file mode 100644 index 0000000..43b3a01 --- /dev/null +++ b/tests/test_host.rs @@ -0,0 +1,209 @@ +mod common; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use form_data_builder::FormData; +use ssh_key::{Algorithm, Certificate}; +use tower::ServiceExt; +use uuid::uuid; + +use sshca::server::make_app; + +use common::setup; +use common::token; + +const ED25519_KEY: &str = concat!( + "ssh-ed25519 ", + "AAAAC3NzaC1lZDI1NTE5AAAAIAsFEmrNIoRHHUayEO0NdAIgtMvci/wME07h+A5XSNJy", +); +const DSA_KEY: &str = concat!( + "ssh-dss ", + "AAAAB3NzaC1kc3MAAACBALNAS+fZjaWt4q+MAgjf6HREFoYjgoSVJUUCtNmRGhND85msVtla", + "kll2gLzL6n6TWyiToARlThoTFu1ZDoGYauDL7iDXrGB6VWJEOQZ3TEMHLFYPziW02AbjR9GI", + "ptsF42D0bTTvvaIaBIhOTjWAUjuFIhAKhPkcj+udIcyH8CG1AAAAFQCpbXQSlxOvd4J92j2C", + "rWDYVGoK8wAAAIAfHiV6/glGZrDRztJmw1hfwbmiNPxaoSGkB+Necfkj0fZrlyLj8sLJIbGQ", + "w0dJMATZdRHw3Ql4R5IOu7sBfX1KQW++onT4ads/Xtl6vwfsjO2e/a6Y1ib9JCIOGJxNAAUC", + "JU0Fm0TSv2Nn6UTICAarp1eKALimqkvy1+ygBWjprgAAAIEAic5EpZH9wpgzvl9kPW531yrz", + "IOlCcXsJFPqQxUThrB2o1g3Rjpscd9kCw5UlPu6GGLk4aSN3UxeIKymTuKiEi7tvP1Tj/Bv5", + "tEc4rhfmrBAfAST09oRFDsELufsOAlTrJ0uk2LhtN14H1RBv9qPR5PQKTEYslyvXG1f8itNQ", + "YnQ=" +); + +fn make_test_request_body(key: &[u8], name: &str) -> (Body, String) { + let mut form = FormData::new(Vec::new()); + form.write_file( + "pubkey", + key, + Some(name.as_ref()), + "application/octet-stream", + ) + .unwrap(); + let content_type = form.content_type_header(); + let body = Body::from(form.finish().unwrap()); + (body, content_type) +} + +fn make_test_request(body: Body, content_type: &str) -> Request { + let hostname = "test.example.org"; + let machine_id = uuid!("b75e9126-d73a-4ae0-9a0d-63cb3552e6cd"); + let token = token::make_token(hostname, machine_id); + Request::builder() + .uri("/host/sign") + .method("POST") + .header("Authorization", format!("Bearer {}", token)) + .header("Host", "sshca.example.org") + .header("Content-Type", content_type) + .body(body) + .unwrap() +} + +#[tokio::test] +async fn test_sign() { + let (ctx, config) = setup::setup().await.unwrap(); + + let app = make_app(config); + let (body, content_type) = make_test_request_body( + ED25519_KEY.as_bytes(), + "ssh_host_ed25519_key.pub", + ); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + let cert = Certificate::from_openssh(std::str::from_utf8(&body).unwrap()) + .unwrap(); + assert_eq!(cert.algorithm(), Algorithm::Ed25519); + cert.validate(&[ctx.host_ca_fingerprint()]).unwrap(); +} + +#[tokio::test] +async fn test_sign_invalid() { + let (_ctx, config) = setup::setup().await.unwrap(); + + let (body, content_type) = make_test_request_body( + "this is not a valid openssh key".as_bytes(), + "ssh_host_ecdsa_key.pub", + ); + let app = make_app(config); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!( + body, + concat!( + "Could not parse SSH key: ", + "Base64 encoding error: invalid Base64 encoding", + ) + ); +} + +#[tokio::test] +async fn test_sign_nokey() { + let (_ctx, config) = setup::setup().await.unwrap(); + + let mut form = FormData::new(Vec::new()); + let content_type = form.content_type_header(); + let body = Body::from(form.finish().unwrap()); + let app = make_app(config); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, "No SSH public key provided in request",); +} + +#[tokio::test] +async fn test_sign_mangled() { + let (_ctx, config) = setup::setup().await.unwrap(); + + let app = make_app(config); + let mut form = FormData::new(Vec::new()); + form.write_file( + "pubkey", + ED25519_KEY.as_bytes(), + Some("ssh_host_ed25519_key.pub".as_ref()), + "application/octet-stream", + ) + .unwrap(); + let content_type = form.content_type_header(); + let mut form_bytes = form.finish().unwrap(); + form_bytes.truncate(19); + let body = Body::from(form_bytes); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, "Error parsing `multipart/form-data` request",); +} + +#[tokio::test] +async fn test_sign_bad_request() { + let (_ctx, config) = setup::setup().await.unwrap(); + + let app = make_app(config); + let content_type = "text/plain"; + let body = Body::from("test"); + let req = make_test_request(body, content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, "Invalid `boundary` for `multipart/form-data` request",); +} + +#[tokio::test] +async fn test_sign_dsa() { + let (_ctx, config) = setup::setup().await.unwrap(); + + let app = make_app(config); + let (body, content_type) = + make_test_request_body(DSA_KEY.as_bytes(), "ssh_host_dsa_key.pub"); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, "Unsupported key algorithm: ssh-dss"); +} + +#[tokio::test] +async fn test_sign_failure() { + let (_ctx, mut config) = setup::setup().await.unwrap(); + config.ca.host.private_key_file = "bogus".into(); + + let app = make_app(config); + let (body, content_type) = make_test_request_body( + ED25519_KEY.as_bytes(), + "ssh_host_ed25519_key.pub", + ); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, "Service Unavailable"); +} + +#[tokio::test] +async fn test_sign_unauthorized() { + // Deliberately drop the TestContext so the machine ID file gets deleted, + // which will cause authentication to fail. + let (_, config) = setup::setup().await.unwrap(); + + let app = make_app(config); + let (body, content_type) = make_test_request_body( + ED25519_KEY.as_bytes(), + "ssh_host_ed25519_key.pub", + ); + let req = make_test_request(body, &content_type); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + let body = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, "Unauthorized"); +}