diff --git a/Cargo.lock b/Cargo.lock index db8b8ae..dbe0f60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -209,6 +209,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -381,6 +387,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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 = "clap" version = "4.5.20" @@ -517,6 +533,18 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -527,6 +555,32 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "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 2.0.86", +] + [[package]] name = "darling" version = "0.20.10" @@ -672,6 +726,41 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + [[package]] name = "either" version = "1.13.0" @@ -681,6 +770,25 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -757,6 +865,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "figment" version = "0.10.19" @@ -949,6 +1073,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -974,6 +1099,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[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.26" @@ -1226,6 +1362,15 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1657,6 +1802,44 @@ 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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "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 = "p5x" version = "0.1.0" @@ -1667,12 +1850,14 @@ dependencies = [ "futures", "log", "proxmox-api", + "rand", "rocket", "sea-orm", "sea-orm-migration", "sea-orm-rocket", "serde", "serde_json", + "ssh-key", "ssh2", "tokio", "ureq", @@ -1831,6 +2016,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2090,6 +2284,16 @@ dependencies = [ "bytecheck", ] +[[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.8" @@ -2230,6 +2434,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", + "sha2", "signature", "spki", "subtle", @@ -2258,6 +2463,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.38" @@ -2521,6 +2735,20 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[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 = "security-framework" version = "2.11.1" @@ -2544,6 +2772,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" + [[package]] name = "serde" version = "1.0.214" @@ -2930,6 +3164,48 @@ dependencies = [ "uuid", ] +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "ed25519-dalek", + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "ssh2" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 9793e31..a60bd46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,5 @@ serde_json = "1.0.132" proxmox-api = { git = "https://github.com/glmdev/p5x-proxmox-api", version = "0.1.2-pre", features = ["ureq-client"] } ureq = "2.10.1" dotenv = "0.15.0" +ssh-key = { version = "0.6.7", features = ["ed25519"] } +rand = "0.8.5" diff --git a/requests/system.http b/requests/system.http new file mode 100644 index 0000000..e363e0b --- /dev/null +++ b/requests/system.http @@ -0,0 +1,4 @@ +GET http://localhost:3450/system/pubkey +Accept: text/plain + +### diff --git a/src/api/cluster/mod.rs b/src/api/cluster/mod.rs index e358360..2d6449d 100644 --- a/src/api/cluster/mod.rs +++ b/src/api/cluster/mod.rs @@ -1,3 +1,4 @@ pub mod node; pub mod carrier; pub mod volume; +pub mod system; diff --git a/src/api/cluster/system.rs b/src/api/cluster/system.rs new file mode 100644 index 0000000..0c24d3e --- /dev/null +++ b/src/api/cluster/system.rs @@ -0,0 +1,42 @@ +use std::env; +use std::fs::{File, Permissions}; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use log::info; +use rand::rngs::OsRng; +use ssh_key::{PrivateKey, Algorithm, LineEnding}; + + +/** Check if the SSH pubkey/privkey exist at the configured paths. If not, generate them. */ +pub fn ensure_ssh_keypair() -> Result<(), ssh_key::Error> { + let pubkey_path = env::var("P5X_SSH_PUBKEY_PATH").expect("Missing env: P5X_SSH_PUBKEY_PATH"); + let privkey_path = env::var("P5X_SSH_PRIVKEY_PATH").expect("Missing env: P5X_SSH_PRIVKEY_PATH"); + + // If both exist, then p5x will boot correctly when it reads from the files. + // If only one of the two files exists, p5x will error on boot. This is safer + // than accidentally overwriting someone's key. + if Path::new(&pubkey_path).exists() || Path::new(&privkey_path).exists() { + info!(target: "p5x", "Found existing SSH keypair on filesystem"); + return Ok(()); + } + + // Generate an ed25519 keypair + info!(target: "p5x", "Generating a new SSH keypair"); + let mut csprng = OsRng; + let privkey = PrivateKey::random(&mut csprng, Algorithm::Ed25519)?; + + // Write the privkey to a file + let privkey_pem = privkey.to_openssh(LineEnding::LF)?; + let mut privkey_file = File::create(privkey_path)?; + privkey_file.write_all(privkey_pem.as_bytes())?; + privkey_file.set_permissions(Permissions::from_mode(0o600))?; + + // Write the pubkey to a file + let pubkey = privkey.public_key(); + let pubkey_ssh = pubkey.to_openssh()?; + let mut pubkey_file = File::create(pubkey_path)?; + pubkey_file.write_all(pubkey_ssh.as_bytes())?; + + Ok(()) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 339b147..4d95633 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,7 +3,7 @@ use rocket::fairing::AdHoc; mod db; mod route; pub mod util; -mod cluster; +pub mod cluster; pub mod entity; pub use db::Db; pub mod services; diff --git a/src/api/route/mod.rs b/src/api/route/mod.rs index bbf1ef3..ab56a24 100644 --- a/src/api/route/mod.rs +++ b/src/api/route/mod.rs @@ -2,10 +2,12 @@ use rocket::fairing::AdHoc; mod volume; mod node; +mod system; pub(super) fn init() -> AdHoc { AdHoc::on_ignite("Registering routes", |rocket| async { rocket.attach(volume::init()) .attach(node::init()) + .attach(system::init()) }) } diff --git a/src/api/route/system.rs b/src/api/route/system.rs new file mode 100644 index 0000000..3fa1ff0 --- /dev/null +++ b/src/api/route/system.rs @@ -0,0 +1,15 @@ +use rocket::fairing::AdHoc; +use rocket::response::status; +use crate::api::util::read_p5x_config; + +#[get("/pubkey")] +fn get_pubkey() -> Result> { + let config = read_p5x_config(); + Ok(config.ssh_pubkey) +} + +pub(super) fn init() -> AdHoc { + AdHoc::on_ignite("Routes: /system", |rocket| async { + rocket.mount("/system", routes![get_pubkey]) + }) +} diff --git a/src/main.rs b/src/main.rs index 90893c6..d622a4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use dotenv::dotenv; use rocket::{Build, Rocket}; use log::{error, info}; use std::{env, process}; +use crate::api::cluster::system::ensure_ssh_keypair; use crate::api::util::read_p5x_config; fn configure_rocket() -> Rocket { @@ -23,6 +24,8 @@ async fn main() { process::exit(1); } + ensure_ssh_keypair().expect("Could not ensure SSH keypair exists."); + let config = read_p5x_config(); // Do this so we early-fail if there are missing env vars info!(target: "p5x", "Successfully read config from environment."); info!(target: "p5x", "Cluster host: {} ({})", config.pve_host_name, config.pve_api_host);