diff --git a/Cargo.lock b/Cargo.lock index 8f33b058..c8883556 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -813,6 +813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -892,9 +893,9 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", "serde", "sha2 0.10.8", + "signature", "subtle", "zeroize", ] @@ -1641,15 +1642,12 @@ dependencies = [ "clap", "clap_derive", "color-eyre", - "ed25519-dalek", "eyre", "fjall", "moor-db", "moor-kernel", "moor-values", "oneshot", - "pem", - "rand", "rpc-common", "rusty_paseto", "semver", @@ -1760,6 +1758,7 @@ dependencies = [ "clap", "clap_derive", "color-eyre", + "ed25519-dalek", "escargot", "eyre", "futures-util", @@ -2010,13 +2009,12 @@ dependencies = [ ] [[package]] -name = "pem" -version = "3.0.4" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "base64", - "serde", + "base64ct", ] [[package]] @@ -2367,8 +2365,8 @@ dependencies = [ "bincode", "clap", "clap_derive", + "ed25519-dalek", "moor-values", - "pem", "rusty_paseto", "thiserror 2.0.8", ] diff --git a/Cargo.toml b/Cargo.toml index c9e774b0..8f173e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,9 +97,9 @@ lazy_static = "1.5" num-traits = "0.2" oneshot = { version = "0.1", default-features = false, features = ["std"] } semver = "1.0.24" -strum = { version = "0.26", features = ["derive"] } similar = "*" similar-asserts = "*" +strum = { version = "0.26", features = ["derive"] } ustr = "1.0" uuid = { version = "1.11", features = ["v4"] } xml-rs = "0.8.24" @@ -141,8 +141,7 @@ test_each_file = "0.3" unindent = "0.2" # Auth/Auth -ed25519-dalek = { version = "2.1", features = ["zeroize", "pkcs8", "rand_core"] } -pem = "3.0" +ed25519-dalek = { version = "2.0", features = ["pkcs8", "pem", "signature"] } rusty_paseto = { version = "0.7" } signal-hook = "0.3" diff --git a/Dockerfile b/Dockerfile index 8334bc2d..a668a6f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,9 @@ FROM rust:1.81-bookworm WORKDIR /moor RUN apt update RUN apt -y install clang-16 libclang-16-dev swig python3-dev cmake libc6 +# Generate the keypair for signing PASETO tokens. Shared between hosts and the daemon. +RUN openssl genpkey -algorithm ed25519 -out moor-signing-key.pem +RUN openssl pkey -in moor-signing-key.pem -pubout -out moor-verifying-key.pem EXPOSE 8080 COPY ./crates ./crates COPY ./Cargo.toml ./Cargo.toml diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index 82a00d78..df9f0c37 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -42,7 +42,4 @@ uuid.workspace = true zmq.workspace = true # Auth/Auth -ed25519-dalek.workspace = true -pem.workspace = true -rand.workspace = true rusty_paseto.workspace = true diff --git a/crates/daemon/src/args.rs b/crates/daemon/src/args.rs index bee9eaae..50ff1bd0 100644 --- a/crates/daemon/src/args.rs +++ b/crates/daemon/src/args.rs @@ -87,27 +87,19 @@ pub struct Args { #[arg( long, value_name = "public_key", - help = "file containing a pkcs8 ed25519 public key, used for authenticating client & host connections", - default_value = "public_key.pem" + help = "file containing the PEM encoded public key (shared with the daemon), used for authenticating client & host connections", + default_value = "moor-verifying-key.pem" )] pub public_key: PathBuf, #[arg( long, value_name = "private_key", - help = "file containing a pkcs8 ed25519 private key, used for authenticating client & host connections", - default_value = "private_key.pem" + help = "file containing an openssh generated ed25519 format private key (shared with the daemon), used for authenticating client & host connections", + default_value = "moor-signing-key.pem" )] pub private_key: PathBuf, - #[arg( - long, - value_name = "generate-keypair", - help = "Generate a new keypair and save it to the keypair files, if they don't exist already", - default_value = "false" - )] - pub generate_keypair: bool, - #[arg( long, value_name = "num-io-threads", diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 50d8b71e..ddf4f202 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -17,16 +17,12 @@ use std::sync::Arc; use crate::args::Args; use crate::rpc_server::RpcServer; use clap::Parser; -use ed25519_dalek::SigningKey; use eyre::Report; use moor_db::{Database, TxDB}; use moor_kernel::tasks::scheduler::Scheduler; use moor_kernel::tasks::{NoopTasksDb, TasksDb}; use moor_kernel::textdump::textdump_load; -use pem::Pem; -use rand::rngs::OsRng; use rpc_common::load_keypair; -use rusty_paseto::core::Key; use tracing::{debug, info, warn}; mod connections; @@ -69,33 +65,14 @@ fn main() -> Result<(), Report> { // Check the public/private keypair file to see if it exists. If it does, parse it and establish // the keypair from it... - let keypair = if args.public_key.exists() && args.private_key.exists() { + let (private_key, public_key) = if args.public_key.exists() && args.private_key.exists() { load_keypair(&args.public_key, &args.private_key) .expect("Unable to load keypair from public and private key files") } else { - // Otherwise, check to see if --generate-keypair was passed. If it was, generate a new - // keypair and save it to the file; otherwise, error out. - if args.generate_keypair { - let mut csprng = OsRng; - let signing_key: SigningKey = SigningKey::generate(&mut csprng); - let keypair: Key<64> = Key::from(signing_key.to_keypair_bytes()); - - let privkey_pem = Pem::new("PRIVATE KEY", signing_key.to_bytes()); - let pubkey_pem = Pem::new("PUBLIC KEY", signing_key.verifying_key().to_bytes()); - - // And write to the files... - std::fs::write(args.private_key.clone(), pem::encode(&privkey_pem)) - .expect("Unable to write private key"); - std::fs::write(args.public_key.clone(), pem::encode(&pubkey_pem)) - .expect("Unable to write public key"); - - keypair - // Write - } else { - panic!( - "Public/private keypair files do not exist, and --generate-keypair was not passed" - ); - } + panic!( + "Public ({:?}) and/or private ({:?}) key files must exist", + args.public_key, args.private_key + ); }; let config = args @@ -164,7 +141,8 @@ fn main() -> Result<(), Report> { .set_io_threads(args.num_io_threads) .expect("Failed to set number of IO threads"); let rpc_server = Arc::new(RpcServer::new( - keypair, + public_key, + private_key, args.connections_file, zmq_ctx.clone(), args.events_listen.as_str(), diff --git a/crates/daemon/src/rpc_server.rs b/crates/daemon/src/rpc_server.rs index c7acac78..c7bd9735 100644 --- a/crates/daemon/src/rpc_server.rs +++ b/crates/daemon/src/rpc_server.rs @@ -59,7 +59,8 @@ use zmq::{Socket, SocketType}; pub struct RpcServer { zmq_context: zmq::Context, - keypair: Key<64>, + public_key: Key<32>, + private_key: Key<64>, pub(crate) events_publish: Arc>, connections: Arc, task_handles: Mutex>, @@ -95,7 +96,8 @@ pub(crate) fn pack_host_response(result: Result, + public_key: Key<32>, + private_key: Key<64>, connections_db_path: PathBuf, zmq_context: zmq::Context, narrative_endpoint: &str, @@ -121,7 +123,8 @@ impl RpcServer { ); let kill_switch = Arc::new(AtomicBool::new(false)); Self { - keypair, + public_key, + private_key, connections, events_publish: Arc::new(Mutex::new(publish)), zmq_context, @@ -1235,7 +1238,7 @@ impl RpcServer { /// validate the client connection to the daemon for future requests. fn make_client_token(&self, client_id: Uuid) -> ClientToken { let privkey: PasetoAsymmetricPrivateKey = - PasetoAsymmetricPrivateKey::from(self.keypair.as_ref()); + PasetoAsymmetricPrivateKey::from(self.private_key.as_ref()); let token = Paseto::::default() .set_footer(Footer::from(MOOR_SESSION_TOKEN_FOOTER)) .set_payload(Payload::from( @@ -1256,7 +1259,7 @@ impl RpcServer { /// Construct a PASETO token for this player login. This token is used to provide credentials /// for requests, to allow reconnection with a different client_id. fn make_auth_token(&self, oid: &Obj) -> AuthToken { - let privkey = PasetoAsymmetricPrivateKey::from(self.keypair.as_ref()); + let privkey = PasetoAsymmetricPrivateKey::from(self.private_key.as_ref()); let token = Paseto::::default() .set_footer(Footer::from(MOOR_AUTH_TOKEN_FOOTER)) .set_payload(Payload::from( @@ -1284,8 +1287,8 @@ impl RpcServer { } } } - let key: Key<32> = Key::from(&self.keypair[32..]); - let pk: PasetoAsymmetricPublicKey = PasetoAsymmetricPublicKey::from(&key); + let pk: PasetoAsymmetricPublicKey = + PasetoAsymmetricPublicKey::from(&self.public_key); let host_type = Paseto::::try_verify( token.0.as_str(), &pk, @@ -1325,8 +1328,8 @@ impl RpcServer { } } - let key: Key<32> = Key::from(&self.keypair[32..]); - let pk: PasetoAsymmetricPublicKey = PasetoAsymmetricPublicKey::from(&key); + let pk: PasetoAsymmetricPublicKey = + PasetoAsymmetricPublicKey::from(&self.public_key); let verified_token = Paseto::::try_verify( token.0.as_str(), &pk, @@ -1391,8 +1394,8 @@ impl RpcServer { } } } - let key: Key<32> = Key::from(&self.keypair[32..]); - let pk: PasetoAsymmetricPublicKey = PasetoAsymmetricPublicKey::from(&key); + let pk: PasetoAsymmetricPublicKey = + PasetoAsymmetricPublicKey::from(&self.public_key); let verified_token = Paseto::::try_verify( token.0.as_str(), &pk, diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 3cd1b4e1..3c6f9015 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -18,10 +18,10 @@ moor-db = { path = "../db" } criterion.workspace = true eyre.workspace = true pretty_assertions.workspace = true -test-case.workspace = true -test_each_file.workspace = true similar.workspace = true similar-asserts.workspace = true +test-case.workspace = true +test_each_file.workspace = true tracing.workspace = true [[test]] diff --git a/crates/rpc/rpc-async-client/src/lib.rs b/crates/rpc/rpc-async-client/src/lib.rs index ef0ff33c..e95b9da5 100644 --- a/crates/rpc/rpc-async-client/src/lib.rs +++ b/crates/rpc/rpc-async-client/src/lib.rs @@ -35,8 +35,9 @@ pub mod pubsub_client; pub mod rpc_client; /// Construct a PASETO token for this host, to authenticate the host itself to the daemon. -pub fn make_host_token(keypair: &Key<64>, host_type: HostType) -> HostToken { - let privkey: PasetoAsymmetricPrivateKey = PasetoAsymmetricPrivateKey::from(keypair); +pub fn make_host_token(private_key: &Key<64>, host_type: HostType) -> HostToken { + let privkey: PasetoAsymmetricPrivateKey = + PasetoAsymmetricPrivateKey::from(private_key.as_ref()); let token = Paseto::::default() .set_footer(Footer::from(MOOR_HOST_TOKEN_FOOTER)) .set_payload(Payload::from(host_type.id_str())) diff --git a/crates/rpc/rpc-common/Cargo.toml b/crates/rpc/rpc-common/Cargo.toml index 62eadb93..965606a1 100644 --- a/crates/rpc/rpc-common/Cargo.toml +++ b/crates/rpc/rpc-common/Cargo.toml @@ -21,5 +21,5 @@ clap_derive.workspace = true thiserror.workspace = true # Auth/Auth -pem.workspace = true +ed25519-dalek.workspace = true rusty_paseto.workspace = true diff --git a/crates/rpc/rpc-common/src/client_args.rs b/crates/rpc/rpc-common/src/client_args.rs index 94cce6bf..c2fc8368 100644 --- a/crates/rpc/rpc-common/src/client_args.rs +++ b/crates/rpc/rpc-common/src/client_args.rs @@ -37,16 +37,16 @@ pub struct RpcClientArgs { #[arg( long, value_name = "public_key", - help = "file containing the pkcs8 ed25519 public key (shared with the daemon), used for authenticating client & host connections", - default_value = "public_key.pem" + help = "file containing the PEM encoded public key (shared with the daemon), used for authenticating client & host connections", + default_value = "moor-verifying-key.pem" )] pub public_key: PathBuf, #[arg( long, value_name = "private_key", - help = "file containing a pkcs8 ed25519 private key (shared with the daemon), used for authenticating client & host connections", - default_value = "private_key.pem" + help = "file containing an openssh generated ed25519 format private key (shared with the daemon), used for authenticating client & host connections", + default_value = "moor-signing-key.pem" )] pub private_key: PathBuf, } diff --git a/crates/rpc/rpc-common/src/lib.rs b/crates/rpc/rpc-common/src/lib.rs index 013d8c17..eba90605 100644 --- a/crates/rpc/rpc-common/src/lib.rs +++ b/crates/rpc/rpc-common/src/lib.rs @@ -13,10 +13,11 @@ // use bincode::{Decode, Encode}; +use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey}; +use ed25519_dalek::{SigningKey, VerifyingKey}; use moor_values::model::ObjectRef; use moor_values::tasks::{NarrativeEvent, SchedulerError, VerbProgramError}; use moor_values::{Obj, Symbol, Var}; -use pem::PemError; use rusty_paseto::prelude::Key; use std::net::SocketAddr; use std::path::Path; @@ -310,17 +311,19 @@ pub enum ClientsBroadcastEvent { #[derive(Error, Debug)] pub enum KeyError { - #[error("Could not read key from file: {0}")] - ParseError(PemError), + #[error("Could not read PEM-encoded key from file")] + KeyParseError, + #[error("Incorrect key format for key: {0}")] + IncorrectKeyFormat(String), #[error("Could not read key from file: {0}")] ReadError(std::io::Error), } /// Load a keypair from the given public and private key (PEM) files. -pub fn load_keypair(public_key: &Path, private_key: &Path) -> Result, KeyError> { +pub fn load_keypair(public_key: &Path, private_key: &Path) -> Result<(Key<64>, Key<32>), KeyError> { let (Some(pubkey_pem), Some(privkey_pem)) = ( - std::fs::read(public_key).ok(), - std::fs::read(private_key).ok(), + std::fs::read_to_string(public_key).ok(), + std::fs::read_to_string(private_key).ok(), ) else { return Err(KeyError::ReadError(std::io::Error::new( std::io::ErrorKind::NotFound, @@ -328,11 +331,14 @@ pub fn load_keypair(public_key: &Path, private_key: &Path) -> Result, Ke ))); }; - let privkey_pem = pem::parse(privkey_pem).map_err(KeyError::ParseError)?; - let pubkey_pem = pem::parse(pubkey_pem).map_err(KeyError::ParseError)?; - - let mut key_bytes = privkey_pem.contents().to_vec(); - key_bytes.extend_from_slice(pubkey_pem.contents()); + let private_key = + SigningKey::from_pkcs8_pem(&privkey_pem).map_err(|_| KeyError::KeyParseError)?; + let public_key = + VerifyingKey::from_public_key_pem(&pubkey_pem).map_err(|_| KeyError::KeyParseError)?; - Ok(Key::from(&key_bytes[0..64])) + let priv_key: Key<64> = Key::try_from(private_key.to_keypair_bytes()) + .map_err(|_| KeyError::IncorrectKeyFormat("private".to_string()))?; + let pub_key: Key<32> = Key::try_from(public_key.to_bytes()) + .map_err(|_| KeyError::IncorrectKeyFormat("public".to_string()))?; + Ok((priv_key, pub_key)) } diff --git a/crates/telnet-host/Cargo.toml b/crates/telnet-host/Cargo.toml index ae396143..e3bba5d7 100644 --- a/crates/telnet-host/Cargo.toml +++ b/crates/telnet-host/Cargo.toml @@ -44,6 +44,7 @@ termimad.workspace = true # Testing [dev-dependencies] +ed25519-dalek.workspace = true escargot.workspace = true serial_test.workspace = true tempfile.workspace = true diff --git a/crates/telnet-host/src/main.rs b/crates/telnet-host/src/main.rs index 12574651..c691b6a4 100644 --- a/crates/telnet-host/src/main.rs +++ b/crates/telnet-host/src/main.rs @@ -104,9 +104,10 @@ async fn main() -> Result<(), eyre::Error> { .await .expect("Unable to start default listener"); - let keypair = load_keypair(&args.client_args.public_key, &args.client_args.private_key) - .expect("Unable to load keypair from public and private key files"); - let host_token = make_host_token(&keypair, HostType::TCP); + let (private_key, _public_key) = + load_keypair(&args.client_args.public_key, &args.client_args.private_key) + .expect("Unable to load keypair from public and private key files"); + let host_token = make_host_token(&private_key, HostType::TCP); let rpc_client = start_host_session( host_token.clone(), diff --git a/crates/telnet-host/tests/integration_test.rs b/crates/telnet-host/tests/integration_test.rs index 4224786d..d5163aeb 100644 --- a/crates/telnet-host/tests/integration_test.rs +++ b/crates/telnet-host/tests/integration_test.rs @@ -47,7 +47,6 @@ fn start_daemon(workdir: &Path, uuid: Uuid) -> ManagedChild { Command::new(daemon_host_bin()) .arg("--textdump") .arg(test_db_path()) - .arg("--generate-keypair") .arg("--events-listen") .arg(format!("{}{}", NARRATIVE_PATH_ROOT, uuid)) .arg("--rpc-listen") @@ -96,6 +95,18 @@ fn start_telnet_host(workdir: &Path, uuid: Uuid, port: u16) -> ManagedChild { ) } +// Just a keypair generated with openssl to satisfy the daemon for running unit tests... + +const SIGNING_KEY: &str = r#"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEILrkKmddHFUDZqRCnbQsPoW/Wsp0fLqhnv5KNYbcQXtk +-----END PRIVATE KEY----- +"#; + +const VERIFYING_KEY: &str = r#"-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAZQUxGvw8u9CcUHUGLttWFZJaoroXAmQgUGINgbBlVYw= +-----END PUBLIC KEY----- +"#; + // These tests all listen on the same port, so we need to make sure // only one runs at a time. @@ -106,21 +117,16 @@ fn test_moot_with_telnet_host>(moot_file: P) { let uuid = Uuid::new_v4(); let test_workdir = tempfile::TempDir::new().expect("Failed to create temporary directory"); + + // Write the private and public key files in the test workdir + let signing_key_file = test_workdir.path().join("moor-signing-key.pem"); + std::fs::write(&signing_key_file, SIGNING_KEY).expect("Failed to write signing key file"); + let verifying_key_file = test_workdir.path().join("moor-verifying-key.pem"); + std::fs::write(&verifying_key_file, VERIFYING_KEY).expect("Failed to write verifying key file"); + let daemon = Arc::new(Mutex::new(start_daemon(test_workdir.path(), uuid))); daemon.lock().unwrap().assert_running().unwrap(); - // Spin until the generated keypair is available (private_key.pem, public_key.pem in test_workdir) - let start = std::time::Instant::now(); - loop { - if test_workdir.path().join("private_key.pem").exists() { - break; - } - if start.elapsed() > std::time::Duration::from_secs(5) { - panic!("Daemon failed to generate keypair in time for the test"); - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - // Ask the OS for a random unused port. Then immediately drop the listener and use the port // for the telnet host. let listener = TcpListener::bind("0.0.0.0:0").unwrap(); diff --git a/crates/testing/load-tools/src/tx-list-append.rs b/crates/testing/load-tools/src/tx-list-append.rs index d1d3c760..f6195b1c 100644 --- a/crates/testing/load-tools/src/tx-list-append.rs +++ b/crates/testing/load-tools/src/tx-list-append.rs @@ -631,9 +631,10 @@ async fn main() -> Result<(), eyre::Error> { let zmq_ctx = tmq::Context::new(); let kill_switch = Arc::new(AtomicBool::new(false)); - let keypair = load_keypair(&args.client_args.public_key, &args.client_args.private_key) - .expect("Unable to load keypair from public and private key files"); - let host_token = make_host_token(&keypair, HostType::TCP); + let (private, _public) = + load_keypair(&args.client_args.public_key, &args.client_args.private_key) + .expect("Unable to load keypair from public and private key files"); + let host_token = make_host_token(&private, HostType::TCP); let (listeners, _ljh) = setup::noop_listeners_loop().await; diff --git a/crates/testing/load-tools/src/verb-dispatch-load-test.rs b/crates/testing/load-tools/src/verb-dispatch-load-test.rs index b20bf12e..a1bf7c34 100644 --- a/crates/testing/load-tools/src/verb-dispatch-load-test.rs +++ b/crates/testing/load-tools/src/verb-dispatch-load-test.rs @@ -330,9 +330,10 @@ async fn main() -> Result<(), eyre::Error> { let zmq_ctx = tmq::Context::new(); let kill_switch = Arc::new(AtomicBool::new(false)); - let keypair = load_keypair(&args.client_args.public_key, &args.client_args.private_key) - .expect("Unable to load keypair from public and private key files"); - let host_token = make_host_token(&keypair, HostType::TCP); + let (private, _public) = + load_keypair(&args.client_args.public_key, &args.client_args.private_key) + .expect("Unable to load keypair from public and private key files"); + let host_token = make_host_token(&private, HostType::TCP); let (listeners, _ljh) = setup::noop_listeners_loop().await; diff --git a/crates/web-host/src/main.rs b/crates/web-host/src/main.rs index 518dd72d..f1795609 100644 --- a/crates/web-host/src/main.rs +++ b/crates/web-host/src/main.rs @@ -241,9 +241,10 @@ async fn main() -> Result<(), eyre::Error> { let kill_switch = Arc::new(AtomicBool::new(false)); - let keypair = load_keypair(&args.client_args.public_key, &args.client_args.private_key) - .expect("Unable to load keypair from public and private key files"); - let host_token = make_host_token(&keypair, HostType::TCP); + let (private_key, _public_key) = + load_keypair(&args.client_args.public_key, &args.client_args.private_key) + .expect("Unable to load keypair from public and private key files"); + let host_token = make_host_token(&private_key, HostType::TCP); let zmq_ctx = tmq::Context::new(); diff --git a/docker-compose.yml b/docker-compose.yml index 9e313dcb..a6b1f475 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: - RUST_BACKTRACE=1 working_dir: /moor command: > - sh -c "./target/release/moor-daemon development.db --rpc-listen=tcp://0.0.0.0:7899 --events-listen=tcp://0.0.0.0:7898 --textdump=JHCore-DEV-2.db --generate-keypair --textdump-out=out.db" + sh -c "./target/release/moor-daemon development.db --rpc-listen=tcp://0.0.0.0:7899 --events-listen=tcp://0.0.0.0:7898 --textdump=JHCore-DEV-2.db --textdump-out=out.db" ports: # ZMQ ports - "7899:7899"