From 1d0e55816300a081606a1aafa1cf1380c4011cfe Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Thu, 16 Nov 2023 20:10:26 -0600 Subject: [PATCH 1/5] Add SSHCA_CLI_DEBUG_TEST_MACHINE_ID env var When running a debug build, the `sshca host sign` command will now check the `SSHCA_CLI_DEBUG_TEST_MACHINE_ID` environment variable for the value to use as the machine ID. This is useful during development and testing, where the real machine ID is inaccessible or otherwise unavailable. The `SSHCA_CLI_DEBUG_TEST_MACHINE_ID` environment variable is *NOT* used at all in release builds. --- src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main.rs b/src/main.rs index ba98571..3d09fa5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -180,6 +180,13 @@ fn get_hostname() -> Option { } fn get_machine_id() -> Option { + #[cfg(debug_assertions)] + if let Ok(v) = std::env::var("SSHCA_CLI_DEBUG_TEST_MACHINE_ID") { + if let Ok(u) = Uuid::parse_str(&v) { + return Some(u); + } + }; + match std::fs::read_to_string(RPI_SERIAL_PATH) { Ok(s) => match Uuid::parse_str(&format!( "{:0>32}", From c26d67a25b920e67af9d72c3122a268d11e4563c Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Mon, 20 Nov 2023 18:23:35 -0600 Subject: [PATCH 2/5] main: Factor out get_sshca_server_url function The `get_sshca_server_url` function encapsulates the logic of identifying the URL of the SSHCA server. For now, it only considers the `SSHCA_SERVER` environment variable, but eventually, it will also support other configuration methods like a configuration file. Moving this to a separate function will allow other areas of the code to share the same logic. --- src/main.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3d09fa5..c2f425e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,15 +96,7 @@ async fn host_cmd(args: HostArgs) -> MainResult { } async fn sign_key(args: SignArgs) -> MainResult { - let url = match std::env::var("SSHCA_SERVER") { - Ok(v) => v, - Err(std::env::VarError::NotPresent) => { - return Err("SSHCA_SERVER environment variable is not set".into()); - } - Err(std::env::VarError::NotUnicode(_)) => { - return Err("SSHCA_SERVER environment variable is invalid".into()); - } - }; + let url = get_sshca_server_url()?; let Some(hostname) = get_hostname() else { return Err("Hostname must be valid UTF-8".into()); }; @@ -175,6 +167,18 @@ async fn sign_key(args: SignArgs) -> MainResult { Ok(()) } +fn get_sshca_server_url() -> Result { + match std::env::var("SSHCA_SERVER") { + Ok(v) => Ok(v), + Err(std::env::VarError::NotPresent) => { + Err("SSHCA_SERVER environment variable is not set".into()) + } + Err(std::env::VarError::NotUnicode(_)) => { + Err("SSHCA_SERVER environment variable is invalid".into()) + } + } +} + fn get_hostname() -> Option { gethostname::gethostname().into_string().ok() } From 3b55f7418e2ad8643bcca0f3504b6ca025fc3cee Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 21 Nov 2023 20:13:17 -0600 Subject: [PATCH 3/5] user: Add sshca user login command The `sshca user login` command will eventually provide the command-line interface for obtaining user SSH certificates. It initiates the OAuth2 login process, retreiving an OpenID Connect Identity Token from the OpenID Server. This token will be submitted to the SSHCA server to authorize a request to sign a certificate. For now, though, the token is printed to standard output, e.g. to be used in a `curl` request. --- .editorconfig | 5 + Cargo.lock | 1222 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 + src/main.rs | 4 + src/user/callback.html | 40 ++ src/user/login.rs | 325 +++++++++++ src/user/mod.rs | 59 ++ 7 files changed, 1638 insertions(+), 24 deletions(-) create mode 100644 src/user/callback.html create mode 100644 src/user/login.rs create mode 100644 src/user/mod.rs diff --git a/.editorconfig b/.editorconfig index 93f4e5f..a0efe0a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,3 +13,8 @@ max_line_length = 79 max_line_length = 79 indent_style = space indent_size = 4 + +[*.html] +max_line_length = 79 +indent_style = space +indent_size = 2 diff --git a/Cargo.lock b/Cargo.lock index 690a583..c428262 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "argh" version = "0.1.12" @@ -90,6 +105,18 @@ dependencies = [ "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" @@ -132,12 +159,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + [[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" @@ -153,12 +196,49 @@ dependencies = [ "libc", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "core-foundation" version = "0.9.3" @@ -184,6 +264,18 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-bigint" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -194,6 +286,90 @@ dependencies = [ "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", + "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", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -201,10 +377,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[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 = [ + "pkcs8", + "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", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[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", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -214,6 +461,12 @@ 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" @@ -221,7 +474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -230,6 +483,22 @@ 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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69037fe1b785e84986b4f2cbcf647381876a00671d25ceef715d7812dd7e1dd" + [[package]] name = "fnv" version = "1.0.7" @@ -275,6 +544,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[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" @@ -294,9 +569,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -307,6 +585,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -316,7 +595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -326,8 +605,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -336,6 +617,41 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "globset" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[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 = "h2" version = "0.3.21" @@ -348,7 +664,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -361,6 +677,51 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "http" version = "0.2.9" @@ -432,6 +793,35 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -442,6 +832,23 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -449,7 +856,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", + "serde", ] [[package]] @@ -458,12 +877,43 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.65" @@ -479,7 +929,7 @@ version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" dependencies = [ - "base64", + "base64 0.21.5", "ring", "serde", "serde_json", @@ -490,6 +940,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" @@ -497,6 +950,12 @@ 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 = "linux-raw-sys" version = "0.4.10" @@ -509,6 +968,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -557,7 +1025,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -578,6 +1046,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -588,6 +1062,93 @@ dependencies = [ "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 = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom", + "http", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "object" version = "0.32.1" @@ -603,6 +1164,38 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openidconnect" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d6050f6a84b81f23c569f5607ad883293e57491036e318fafe6fc4895fadb1" +dependencies = [ + "base64 0.13.1", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http", + "itertools", + "log", + "oauth2", + "p256", + "p384", + "rand", + "rsa", + "serde", + "serde-value", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror", + "url", +] + [[package]] name = "openssl" version = "0.10.59" @@ -647,12 +1240,45 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[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 = "password-hash" version = "0.5.0" @@ -664,12 +1290,66 @@ dependencies = [ "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 = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -682,12 +1362,60 @@ 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 = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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" @@ -706,11 +1434,41 @@ 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 = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "redox_syscall" @@ -771,7 +1529,7 @@ version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -804,6 +1562,16 @@ dependencies = [ "winreg", ] +[[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.17.5" @@ -813,9 +1581,29 @@ dependencies = [ "cc", "getrandom", "libc", - "spin", + "spin 0.9.8", "untrusted", - "windows-sys", + "windows-sys 0.48.0", +] + +[[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", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -824,6 +1612,15 @@ 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" @@ -834,7 +1631,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -843,13 +1640,36 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", +] + +[[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]] @@ -875,6 +1695,12 @@ dependencies = [ "libc", ] +[[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" @@ -884,6 +1710,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.190" @@ -906,6 +1742,25 @@ dependencies = [ "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_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -918,6 +1773,46 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[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" @@ -927,6 +1822,16 @@ dependencies = [ "lazy_static", ] +[[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" @@ -959,31 +1864,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] +[[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 = "sshca-cli" -version = "0.1.0" +version = "0.1.1" dependencies = [ "argh", "argon2", + "form_urlencoded", "gethostname", + "hyper", "jsonwebtoken", + "openidconnect", "reqwest", "serde", + "serde_json", + "tera", + "thiserror", "tokio", "tracing", "tracing-subscriber", "uuid", + "webbrowser", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -1032,7 +1966,43 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "globwalk", + "lazy_static", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "unic-segment", +] + +[[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]] @@ -1045,6 +2015,35 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1070,10 +2069,11 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "pin-project-lite", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1190,6 +2190,62 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.7.0" @@ -1235,6 +2291,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1261,6 +2318,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1352,6 +2419,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle", + "url", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1368,19 +2452,61 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -1389,51 +2515,93 @@ 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", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1447,5 +2615,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] + +[[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 index e799ac6..7609160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,18 @@ edition = "2021" [dependencies] argh = "0.1.12" argon2 = { version = "0.5.2", default-features = false, features = ["alloc"] } +form_urlencoded = "1.2.0" gethostname = "0.4.3" +hyper = { version = "0.14", features = ["server"] } jsonwebtoken = { version = "9.1.0", default-features = false } +openidconnect = { version = "3.4.0", default-features = false, features = ["reqwest", "native-tls"] } reqwest = { version = "0.11.22", features = ["multipart"] } serde = { version = "1.0.190", features = ["derive"] } +serde_json = "1.0.108" +tera = { version = "1.19.1", default-features = false } +thiserror = "1.0.50" tokio = { version = "1.33.0", features = ["rt", "macros"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } uuid = "1.5.0" +webbrowser = "0.8.12" diff --git a/src/main.rs b/src/main.rs index c2f425e..7409178 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +mod user; + use std::path::PathBuf; use std::time; @@ -25,6 +27,7 @@ struct Args { #[argh(subcommand)] enum Subcommand { Host(HostArgs), + User(user::Args), } /// Manage host keys and certificates @@ -86,6 +89,7 @@ async fn inner_main() -> MainResult { let args: Args = argh::from_env(); match args.command { Subcommand::Host(args) => host_cmd(args).await, + Subcommand::User(args) => user::main(args).await, } } diff --git a/src/user/callback.html b/src/user/callback.html new file mode 100644 index 0000000..1894f8e --- /dev/null +++ b/src/user/callback.html @@ -0,0 +1,40 @@ + + + + +SSHCA Login + + + + +{% if error %} +

Login Error

+

+{{ error }} +

+{% else %} +

Login Successful

+

You may now close this window.

+ +{% endif %} + diff --git a/src/user/login.rs b/src/user/login.rs new file mode 100644 index 0000000..9f3124f --- /dev/null +++ b/src/user/login.rs @@ -0,0 +1,325 @@ +//! OpenID Connect User Authentication +//! +//! The SSHCA server uses OIDC Identity Tokens to authorize users in +//! order to issue SSH user certificates. +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use hyper::service; +use hyper::{Body, Request, Response, Server}; +use openidconnect::core::{ + CoreAuthenticationFlow, CoreClient, CoreErrorResponseType, + CoreProviderMetadata, +}; +use openidconnect::reqwest::async_http_client; +use openidconnect::reqwest::Error as OidcReqwestError; +use openidconnect::url::ParseError; +use openidconnect::{ + AccessTokenHash, AuthorizationCode, ClaimsVerificationError, ClientId, + ClientSecret, CsrfToken, DiscoveryError, IssuerUrl, Nonce, + PkceCodeChallenge, RedirectUrl, RequestTokenError, Scope, SigningError, + StandardErrorResponse, +}; +use openidconnect::{OAuth2TokenResponse, TokenResponse}; +use reqwest::Url; +use serde::Deserialize; +use tera::{Context, Tera}; +use tokio::sync::mpsc; +use tokio::sync::Notify; +use tokio::time::error::Elapsed; +use tracing::{debug, error, info, trace}; + +/// Error type for issues during login/authentication +#[derive(Debug, thiserror::Error)] +pub enum LoginError { + #[error("SSHCA Server not configured for OpenID Connect authorization")] + OidcNotConfigured, + #[error("Invalid OIDC IdP URL: cannot be a base")] + UnsupportedUrl, + #[error("Invalid OIDC IdP URL: {0}")] + UrlParse(#[from] ParseError), + #[error("HTTP request error: {0}")] + Request(#[from] reqwest::Error), + #[error("Failed to parse JSON document: {0}")] + JsonParse(#[from] serde_json::Error), + #[error("OIDC discovery failed: {0}")] + OidcDiscovery(#[from] DiscoveryError>), + #[error("Token request error: {0}")] + TokenRequestError( + #[from] + RequestTokenError< + OidcReqwestError, + StandardErrorResponse, + >, + ), + #[error("Server did not return an ID token")] + MissingIdToken, + #[error("Server returned an ID token")] + InvalidIdToken, + #[error("Invalid token claims: {0}")] + ClaimsVerificationError(#[from] ClaimsVerificationError), + #[error("Token signature error: {0}")] + SigningError(#[from] SigningError), + #[error("Invalid request: {0}")] + InvalidRequest(String), + #[error("Missing state parameter")] + MissingStateParam, + #[error("Invalid state parameter")] + InvalidCsrfState, + #[error("Missing OAuth2 authorization code")] + MissingAuthCode, + #[error("{0}")] + IdpError(String), + #[error("Timed out waiting for OAuth2 authorization callback")] + Timeout(#[from] Elapsed), +} + +/// OpenID Connect Client Configuration +/// +/// All fields are optional, as the server may not be configured for +/// OIDC authorization. +#[derive(Deserialize)] +pub struct OidcConfig { + /// OIDC IdP base URL + url: Option, + /// OAuth2 client ID + client_id: Option, + /// OAuth2 client secret + client_secret: Option, +} + +/// Retrieve OpenID Connect client configuration from SSHCA server +/// +/// The SSHCA server provides the necessary configuration values for +/// contacting the OpenID Provider. This function retrieves those +/// values from the server, returning an [`OidcConfig`] structure that +/// can be passed to [`login`]. +/// +/// If an error occurs communicating with the server, [`LoginError`] +/// is returned. +pub async fn get_oidc_config(url: &str) -> Result { + let mut url = Url::parse(url)?; + url.path_segments_mut() + .map_err(|_| LoginError::UnsupportedUrl)? + .pop_if_empty() + .push("user") + .push("oidc-config"); + let client = reqwest::Client::new(); + info!("Fetching SSHCA OIDC configuration"); + debug!("Request: GET {}", url); + let res = client.get(url).send().await?; + debug!("Response: {:?} {}", &res.version(), &res.status()); + res.error_for_status_ref()?; + Ok(serde_json::from_str(&res.text().await?)?) +} + +/// Log in with the OIDC IdP and return an identity token +/// +/// This function performs the OAuth2 login process, requesting an +/// identity token from the OpenID Provider. +/// +/// The OAuth2 login process requires user interaction via a web +/// browser. If possible, this function will launch a browser and +/// navigate to the OIDC authorization URL. If the browser could not +/// be launched automatically, the authorization URL is printed to +/// standard error, where the user must click it or copy & paste it +/// into a browser manually. +/// +/// After initiating the login process, this function starts an HTTP +/// server (usually listening on the loopback interface), in order to +/// handle the request from the IdP after the user has successfully +/// logged in. The request will contain an OAuth2 Authorization Code, +/// which will be converted into an OIDC Identity Token by making an +/// HTTP request to the IdP directly. +pub async fn login( + config: OidcConfig, + listen: Option, + timeout: Option, +) -> Result { + if config.url.is_none() || config.client_id.is_none() { + return Err(LoginError::OidcNotConfigured); + } + let oidc_url = config.url.unwrap(); + let client_id = config.client_id.unwrap(); + + let listen = listen.unwrap_or(([127, 0, 0, 1], 8976).into()); + let timeout = timeout.unwrap_or_else(|| Duration::from_secs(300)); + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(oidc_url)?, + async_http_client, + ) + .await?; + + let client = CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(client_id), + config.client_secret.map(ClientSecret::new), + ) + .set_redirect_uri(RedirectUrl::new(format!("http://{}", listen))?); + + let (pkce_challenge, pkce_verifier) = + PkceCodeChallenge::new_random_sha256(); + trace!("PKCE: {:?} {:?}", pkce_challenge, pkce_verifier); + + let (auth_url, csrf_token, nonce) = client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .set_pkce_challenge(pkce_challenge) + .add_scope(Scope::new("openid".into())) + .add_scope(Scope::new("profile".into())) + .add_scope(Scope::new("email".into())) + .add_scope(Scope::new("groups".into())) + .url(); + trace!( + "CSRF token: {}, nonce: {}", + csrf_token.secret(), + nonce.secret() + ); + + let srv = tokio::spawn(run_server(listen, csrf_token, timeout)); + + if let Err(e) = webbrowser::open(auth_url.as_str()) { + eprintln!("Could not open web browser: {}", e); + eprintln!("Browse to: {}", auth_url); + } + + let code = srv.await.unwrap()?; + trace!("Got authorization code: {}", code); + + info!("Exchanging authorization code for access token"); + let token_response = client + .exchange_code(AuthorizationCode::new(code)) + .set_pkce_verifier(pkce_verifier) + .request_async(async_http_client) + .await?; + debug!("Received response token type {:?}", token_response.token_type()); + debug!("Access token: {}", token_response.access_token().secret()); + trace!("Token response: {:?}", token_response); + + let id_token = token_response + .id_token() + .ok_or(LoginError::MissingIdToken)?; + let claims = id_token.claims(&client.id_token_verifier(), &nonce)?; + + if let Some(expected_access_token_hash) = claims.access_token_hash() { + let actual_access_token_hash = AccessTokenHash::from_token( + token_response.access_token(), + &id_token.signing_alg()?, + )?; + if actual_access_token_hash != *expected_access_token_hash { + return Err(LoginError::InvalidIdToken); + } + } + + Ok(id_token.to_string()) +} + +/// Start HTTP server for OAuth2 callback +/// +/// After the user logs in, the OAuth2 IdP redirects the browser to the +/// URL provided in the authorization request. This function starts +/// an HTTP server, listening on the specified socket address (usually +/// some port on the loopback interface) to receive the callback +/// request. Only a single request is handled, after which the server +/// is stopped and the OAuth2 Authorization Code included in the +/// query string of the request is parsed and returned. +/// +/// If an error occurs while running the server or handling the request, +/// [`LoginError`] is returned. +async fn run_server( + listen: SocketAddr, + csrf_token: CsrfToken, + timeout: Duration, +) -> Result { + let csrf_token = Arc::new(csrf_token); + let (tx, mut rx) = mpsc::channel(1); + let notify = Arc::new(Notify::new()); + let notifier = notify.clone(); + + let svc = service::make_service_fn(move |_| { + let csrf_token = csrf_token.clone(); + let result = tx.clone(); + let notifier = notifier.clone(); + + async move { + Ok::<_, hyper::Error>(service::service_fn(move |req| { + debug!("Handling HTTP request"); + let csrf_token = csrf_token.clone(); + let result = result.clone(); + let notifier = notifier.clone(); + async move { + let mut ctx = Context::new(); + match handle_callback(req, &csrf_token).await { + Ok(s) => { + result.send(Ok(s)).await.unwrap(); + } + Err(e) => { + ctx.insert("error", &e.to_string()); + result.send(Err(e)).await.unwrap(); + } + }; + let mut tera = Tera::default(); + tera.add_raw_template( + "callback", + include_str!("callback.html"), + ) + .unwrap(); + let res = tera.render("callback", &ctx).unwrap(); + notifier.notify_one(); + Ok::<_, hyper::Error>(Response::new(Body::from(res))) + } + })) + } + }); + + debug!("Starting HTTP server on {}", listen); + let server = tokio::spawn( + Server::bind(&listen) + .serve(svc) + .with_graceful_shutdown(async move { notify.notified().await }), + ); + info!("Waiting for callback request"); + let code = tokio::time::timeout(timeout, rx.recv()).await?.unwrap(); + let _ = server.await.unwrap(); + code +} + +/// HTTP request handler for OAuth2 authorization callbacks +/// +/// This function validates the CSRF token and parses the OAuth2 +/// Authorization Code from the callback request. +async fn handle_callback( + req: Request, + csrf_token: &CsrfToken, +) -> Result { + let query = req + .uri() + .query() + .ok_or(LoginError::InvalidRequest("Missing query string".into()))?; + let params: HashMap<_, _> = + form_urlencoded::parse(query.as_bytes()).collect(); + + let state = params.get("state").ok_or(LoginError::MissingStateParam)?; + if state != csrf_token.secret() { + return Err(LoginError::InvalidCsrfState); + } + + if let Some(error) = params.get("error") { + let msg = if let Some(err_desc) = params.get("error_description") { + format!("Error handling ODIC callback ({}): {}", error, err_desc,) + } else { + format!("Error handling OIDC callback: {}", error) + }; + error!("{}", msg); + return Err(LoginError::IdpError(msg)); + } + + let code = params.get("code").ok_or(LoginError::MissingAuthCode)?; + info!("Received OAuth2 authorization code"); + Ok(code.to_string()) +} diff --git a/src/user/mod.rs b/src/user/mod.rs new file mode 100644 index 0000000..24a3cd5 --- /dev/null +++ b/src/user/mod.rs @@ -0,0 +1,59 @@ +//! CLI module for user features +//! +//! The `sshca user` sub-command handles user-based operations, such +//! as signing an SSH user certificate. +mod login; + +use std::time::Duration; + +use argh::FromArgs; + +use crate::MainResult; + +/// Manage host keys and certificates +#[derive(FromArgs)] +#[argh(subcommand, name = "user")] +pub(crate) struct Args { + #[argh(subcommand)] + command: UserSubcommand, +} + +#[derive(FromArgs)] +#[argh(subcommand)] +enum UserSubcommand { + Login(LoginArgs), +} + +/// Log in and obtain an SSH user certificate +#[derive(FromArgs)] +#[argh(subcommand, name = "login")] +struct LoginArgs { + /// listen socket address for OIDC callback (default: 127.0.0.1:8976) + #[argh(option, short = 'l')] + callback_listen_address: Option, + + /// oidc callback timeout, in seconds (default: 300) + #[argh(option, short = 't')] + callback_timeout: Option, +} + +/// Main entry point for `sshca user` +pub(crate) async fn main(args: Args) -> MainResult { + match args.command { + UserSubcommand::Login(args) => login(args).await, + } +} + +/// Entry point for `sshca user login` +async fn login(args: LoginArgs) -> MainResult { + let listen = match args.callback_listen_address { + Some(s) => Some(s.parse()?), + None => None, + }; + let timeout = args.callback_timeout.map(Duration::from_secs); + let url = super::get_sshca_server_url()?; + let config = login::get_oidc_config(&url).await?; + let token = login::login(config, listen, timeout).await?; + println!("{}", token); + Ok(()) +} From 123ca813a7f12145ca1a6a078e538024ba44e423 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Wed, 22 Nov 2023 07:20:12 -0600 Subject: [PATCH 4/5] user/login: Request signed cert from SSHCA The `sshca-cli user login` command now requests a signed certificate from the SSHCA server. Given a valid OpenID Connect identity token and an SSH public key, the server will return a signed certificate, valid for a predetermined (usually short) period of time. The principals listed in the certificate are derived from the ID token. --- Cargo.lock | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/user/cert.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ src/user/mod.rs | 26 ++++++++++++++-- 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 src/user/cert.rs diff --git a/Cargo.lock b/Cargo.lock index c428262..8795f3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[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 = "combine" version = "4.6.6" @@ -871,6 +881,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1279,6 +1298,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1600,6 +1633,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", + "sha2", "signature", "spki", "subtle", @@ -1889,6 +1923,48 @@ dependencies = [ "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f8f4ea73476c0aa5d5e6a75ce1e8634e2c3f82005ef3bbed21547ac57f2bf7" +dependencies = [ + "ed25519-dalek", + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "sshca-cli" version = "0.1.1" @@ -1903,6 +1979,8 @@ dependencies = [ "reqwest", "serde", "serde_json", + "ssh-encoding", + "ssh-key", "tera", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 7609160..0db92af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ openidconnect = { version = "3.4.0", default-features = false, features = ["reqw reqwest = { version = "0.11.22", features = ["multipart"] } serde = { version = "1.0.190", features = ["derive"] } serde_json = "1.0.108" +ssh-encoding = "0.2.0" +ssh-key = { version = "0.6.4", features = ["ed25519"] } tera = { version = "1.19.1", default-features = false } thiserror = "1.0.50" tokio = { version = "1.33.0", features = ["rt", "macros"] } diff --git a/src/user/cert.rs b/src/user/cert.rs new file mode 100644 index 0000000..2deefa3 --- /dev/null +++ b/src/user/cert.rs @@ -0,0 +1,74 @@ +//! SSHCA User Certificates +//! +//! The SSHCA server will sign SSH certificates for a user given a +//! supported public key and a valid OIDC Identity token. +use reqwest::multipart::{Form, Part}; +use reqwest::{StatusCode, Url}; +use ssh_key::{Certificate, PublicKey}; +use tracing::{debug, error, info}; + +/// Error type for issues requesting a signed SSH certificate +#[derive(Debug, thiserror::Error)] +pub enum SignError { + #[error("{0}")] + BadUrl(String), + #[error("SSH key error: {0}")] + SshKey(#[from] ssh_key::Error), + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + #[error("Bad request: {0}")] + BadRequest(String), +} + +/// Request a signed SSH certificate for a given public key +/// +/// This function requests the SSHCA server to sign a certificate for +/// the specified SSH public key. The server will use the provided +/// identity token to authorize the request and return a certificate +/// for the user based on the token subject. +/// +/// If an error occurs while requesting the certificate, [`SignError`] +/// is returned. +pub async fn sign_key( + token: &str, + pubkey: &PublicKey, +) -> Result { + let url = crate::get_sshca_server_url().map_err(SignError::BadUrl)?; + + let mut url = + Url::parse(&url).map_err(|e| SignError::BadUrl(e.to_string()))?; + url.path_segments_mut() + .map_err(|_| SignError::BadUrl("Invalid URL: missing host".into()))? + .pop_if_empty() + .push("user") + .push("sign"); + + let key_str = pubkey.to_openssh()?; + let form = Form::new().part("pubkey", Part::text(key_str)); + + let client = reqwest::Client::new(); + info!("Requesting SSH user certificate"); + debug!("Request: POST {}", url); + let res = client + .post(url) + .header("Authorization", format!("Bearer {}", token)) + .multipart(form) + .send() + .await?; + debug!("Response: {:?} {}", &res.version(), &res.status()); + match res.error_for_status_ref() { + Ok(_) => (), + Err(e) if e.status() == Some(StatusCode::BAD_REQUEST) => { + let msg = res.text().await.unwrap_or_else(|e| e.to_string()); + error!("{}: {}", e, msg); + return Err(SignError::BadRequest(format!("{}\n{}", e, msg))); + } + Err(e) => { + error!("{}", e); + return Err(e.into()); + } + }; + let cert = Certificate::from_openssh(&res.text().await?)?; + + Ok(cert) +} diff --git a/src/user/mod.rs b/src/user/mod.rs index 24a3cd5..0d8a927 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -2,11 +2,15 @@ //! //! The `sshca user` sub-command handles user-based operations, such //! as signing an SSH user certificate. +mod cert; mod login; use std::time::Duration; use argh::FromArgs; +use ssh_key::rand_core::OsRng; +use ssh_key::{Algorithm, PrivateKey}; +use tracing::debug; use crate::MainResult; @@ -33,8 +37,16 @@ struct LoginArgs { callback_listen_address: Option, /// oidc callback timeout, in seconds (default: 300) - #[argh(option, short = 't')] + #[argh(option, short = 'T')] callback_timeout: Option, + + /// ssh key type + #[argh(option, short = 't', default = "default_key_type()")] + key_type: String, +} + +fn default_key_type() -> String { + "ssh-ed25519".into() } /// Main entry point for `sshca user` @@ -46,14 +58,24 @@ pub(crate) async fn main(args: Args) -> MainResult { /// Entry point for `sshca user login` async fn login(args: LoginArgs) -> MainResult { + let algo = Algorithm::new(&args.key_type)?; let listen = match args.callback_listen_address { Some(s) => Some(s.parse()?), None => None, }; let timeout = args.callback_timeout.map(Duration::from_secs); + let url = super::get_sshca_server_url()?; let config = login::get_oidc_config(&url).await?; let token = login::login(config, listen, timeout).await?; - println!("{}", token); + + debug!("Generatring new {} key pair", algo); + let privkey = PrivateKey::random(&mut OsRng, algo) + .map_err(|e| format!("Error generating key pair: {0}", e))?; + let pubkey = privkey.public_key(); + let cert = cert::sign_key(&token, pubkey).await?; + if let Ok(c) = cert.to_openssh() { + println!("{}", c); + } Ok(()) } From d443542ee00020084fee4c02f72483141f707822 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 30 Jan 2024 21:00:17 -0600 Subject: [PATCH 5/5] user/login: Add cert to SSH agent An SSH certificate is useless on its own, as without the private key, clients cannot sign servers' authentication requests. Since `sshca-cli user login` creates a new key pair each time it is run, the private key needs to be kept at least as long as the certificate is valid. To that end, the command will now add both to the user's SSH agent. It communicates with the agent via the UNIX stream socket specified by the `SSH_AUTH_SOCK` environment variable. Although there is a Rust crate, [ssh-agent-client-rs][0] that implements the client side of the SSH agent protocol, it does not support adding certificates to the agent. In fact, that functionality is not even documented in the IETF draft specification for the protocol. Thus, I had to figure it out by reading the code of the OpenSSH `ssh-add` tool, and observing the messages passed between it and `ssh-agent`. [0]: https://crates.io/crates/ssh-agent-client-rs --- Cargo.toml | 2 +- src/user/agent.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++ src/user/mod.rs | 11 ++-- 3 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/user/agent.rs diff --git a/Cargo.toml b/Cargo.toml index 0db92af..3dd4f8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ ssh-encoding = "0.2.0" ssh-key = { version = "0.6.4", features = ["ed25519"] } tera = { version = "1.19.1", default-features = false } thiserror = "1.0.50" -tokio = { version = "1.33.0", features = ["rt", "macros"] } +tokio = { version = "1.33.0", features = ["rt", "macros", "net"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } uuid = "1.5.0" diff --git a/src/user/agent.rs b/src/user/agent.rs new file mode 100644 index 0000000..3a7dc0e --- /dev/null +++ b/src/user/agent.rs @@ -0,0 +1,133 @@ +//! SSH Agent client +//! +//! The `sshca-cli user login` command will automatically add the +//! signed certificate it received from SSHCA to the user's SSH agent. + +use ssh_encoding::{Encode, Writer}; +use ssh_key::{private::KeypairData, Certificate, PrivateKey}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UnixStream; +use tracing::{debug, info, trace}; + +/// Error type for SSH agent communication +#[derive(Debug, thiserror::Error)] +pub enum AgentError { + #[error("SSH agent not found: {0}")] + NotFound(std::env::VarError), + #[error("Could not connect to SSH agent: {0}")] + Connect(std::io::Error), + #[error("Encoding error: {0}")] + Encoding(#[from] ssh_encoding::Error), + #[error("Invalid message length: {0}")] + IvalidMessageLength(#[from] std::num::TryFromIntError), + #[error("Error communicating with SSH agent: {0}")] + Io(#[from] std::io::Error), + #[error("SSH agent returned failure")] + Failure, +} + +/// SSH Agent message types +#[repr(u8)] +enum AgentMessageType { + AddIdentity = 17, +} + +/// SSH Agent response code +enum AgentResponseCode { + Success, + Failure, + InvalidFormat, +} + +impl From for AgentResponseCode { + fn from(v: u8) -> Self { + // https://github.com/openssh/openssh-portable/blob/V_9_0_P1/authfd.c#L74 + match v { + 6 => Self::Success, + 30 | 102 | 229 => Self::Failure, + _ => Self::InvalidFormat, + } + } +} + +/// Add an SSH certificate and private key to the SSH Agent +/// +/// This function adds an SSH certificate and its corresponding private +/// key to the SSH Agent. +/// +/// If an error occurs while attempting to add the key to the agent, +/// including if no agent is configured, [`AgentError`] is returned. +pub async fn add_to_agent( + key: &PrivateKey, + cert: &Certificate, +) -> Result<(), AgentError> { + let sock_path = + std::env::var("SSH_AUTH_SOCK").map_err(AgentError::NotFound)?; + debug!("Connecting to SSH agent at {:?}", sock_path); + let mut sock = UnixStream::connect(sock_path) + .await + .map_err(AgentError::Connect)?; + + trace!("Serializing SSH2_AGENTC_ADD_IDENTITY message"); + let mut buf: Vec = vec![]; + buf.push(AgentMessageType::AddIdentity as u8); + serialize_key_cert(key, cert, &mut buf)?; + let len = u32::try_from(buf.len())?; + + debug!("Sending key to SSH agent"); + sock.write_all(&len.to_be_bytes()).await?; + sock.write_all(&buf[..]).await?; + + let mut res_len = [0u8; 4]; + trace!("Waiting for SSH agent response"); + sock.read_exact(&mut res_len[..]).await?; + let res_len = usize::try_from(u32::from_be_bytes(res_len))?; + let mut res = vec![0u8; res_len]; + trace!("Reading {} bytes from SSH agent", res_len); + sock.read_exact(&mut res).await?; + trace!("Received SSH agent response: {:?}", res); + + match res[0].into() { + AgentResponseCode::Success => { + info!("Successfully added SSH user certificate to SSH Agent"); + Ok(()) + } + _ => Err(AgentError::Failure), + } +} + +/// Serialize an SSH certificate and private key to send to SSH Agent +/// +/// This function takes a byte buffer and fills it with the wire +/// representation of an SSH certificate and its corresponding provate +/// key, in order to send them to the SSH Agent. +/// +/// The [draft-miller-ssh-agent-11][0] protocol does not specify how +/// certificates are sent to the SSH Agent. The message format used +/// here was discovered by reading the OpenSSH portable code, +/// specifically [sshkey.c][1], and observing communications between +/// `ssh-add` and `ssh-agent`. +/// +/// [0]: https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-11 +/// [1]: https://github.com/openssh/openssh-portable/blob/V_9_0_P1/sshkey.c#L3230 +fn serialize_key_cert( + key: &PrivateKey, + cert: &Certificate, + buf: &mut impl Writer, +) -> Result<(), ssh_encoding::Error> { + cert.algorithm().to_certificate_type().encode(buf)?; + cert.encode_prefixed(buf)?; + match key.key_data() { + KeypairData::Dsa(k) => k.encode(buf)?, + KeypairData::Ecdsa(k) => k.encode(buf)?, + KeypairData::Ed25519(k) => k.encode(buf)?, + KeypairData::Encrypted(k) => k.encode(buf)?, + KeypairData::Rsa(k) => k.encode(buf)?, + KeypairData::SkEcdsaSha2NistP256(k) => k.encode(buf)?, + KeypairData::SkEd25519(k) => k.encode(buf)?, + KeypairData::Other(k) => k.encode(buf)?, + &_ => todo!(), + }; + key.comment().encode(buf)?; + Ok(()) +} diff --git a/src/user/mod.rs b/src/user/mod.rs index 0d8a927..55f9082 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -2,6 +2,7 @@ //! //! The `sshca user` sub-command handles user-based operations, such //! as signing an SSH user certificate. +mod agent; mod cert; mod login; @@ -10,7 +11,7 @@ use std::time::Duration; use argh::FromArgs; use ssh_key::rand_core::OsRng; use ssh_key::{Algorithm, PrivateKey}; -use tracing::debug; +use tracing::{debug, error}; use crate::MainResult; @@ -74,8 +75,12 @@ async fn login(args: LoginArgs) -> MainResult { .map_err(|e| format!("Error generating key pair: {0}", e))?; let pubkey = privkey.public_key(); let cert = cert::sign_key(&token, pubkey).await?; - if let Ok(c) = cert.to_openssh() { - println!("{}", c); + + if let Err(e) = agent::add_to_agent(&privkey, &cert).await { + error!("Failed to add certificate to SSH agent: {}", e); + if let Ok(c) = cert.to_openssh() { + println!("{}", c); + } } Ok(()) }