From 5359ba039ba5e789e93300d9d3f9f8bdc704ed8f Mon Sep 17 00:00:00 2001 From: ghe0 Date: Mon, 3 Feb 2025 16:25:29 +0200 Subject: [PATCH] added auth --- Cargo.lock | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + snp.proto | 28 ++++--- src/data.rs | 83 +++++++++++-------- src/grpc.rs | 228 ++++++++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 496 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af7229c..ad4f555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,18 +209,35 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "brain-mock" version = "0.1.0" dependencies = [ + "bs58", "chrono", "dashmap", + "ed25519-dalek", "env_logger", "log", "prost", @@ -236,6 +253,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -289,6 +315,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -305,12 +337,58 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.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", + "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 = "dashmap" version = "6.1.0" @@ -325,6 +403,26 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -336,6 +434,30 @@ dependencies = [ "syn", ] +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" @@ -396,6 +518,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -471,6 +599,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1112,6 +1250,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[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.31" @@ -1340,6 +1488,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.42" @@ -1442,6 +1599,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.216" @@ -1486,12 +1649,32 @@ dependencies = [ "serde", ] +[[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 = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -1523,6 +1706,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1630,6 +1823,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.42.0" @@ -1829,6 +2037,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.14" @@ -1885,6 +2099,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 5ef1cd6..00e4298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] +bs58 = "0.5.1" chrono = "0.4.39" dashmap = "6.1.0" +ed25519-dalek = "2.1.1" env_logger = "0.11.6" log = "0.4.22" prost = "0.13.4" diff --git a/snp.proto b/snp.proto index 849a656..fa9654e 100644 --- a/snp.proto +++ b/snp.proto @@ -52,6 +52,7 @@ message MeasurementIP { string gateway = 4; } +// This should also include a block hash or similar, for auth message RegisterNodeReq { string node_pubkey = 1; string owner_pubkey = 2; @@ -101,13 +102,14 @@ message NewVmResp { message UpdateVmReq { string uuid = 1; - uint32 disk_size_gb = 2; - uint32 vcpus = 3; - uint32 memory_mb = 4; - string kernel_url = 5; - string kernel_sha = 6; - string dtrfs_url = 7; - string dtrfs_sha = 8; + string admin_pubkey = 2; + uint32 disk_size_gb = 3; + uint32 vcpus = 4; + uint32 memory_mb = 5; + string kernel_url = 6; + string kernel_sha = 7; + string dtrfs_url = 8; + string dtrfs_sha = 9; } message UpdateVmResp { @@ -118,6 +120,7 @@ message UpdateVmResp { message DeleteVmReq { string uuid = 1; + string admin_pubkey = 2; } message BrainMessage { @@ -128,9 +131,16 @@ message BrainMessage { } } +message DaemonStreamAuth { + string timestamp = 1; + string pubkey = 2; + repeated string contracts = 3; + string signature = 4; +} + message DaemonMessage { oneof Msg { - Pubkey pubkey = 1; + DaemonStreamAuth auth = 1; NewVmResp new_vm_resp = 2; UpdateVmResp update_vm_resp = 3; NodeResources node_resources = 4; @@ -139,7 +149,7 @@ message DaemonMessage { service BrainDaemon { rpc RegisterNode (RegisterNodeReq) returns (stream Contract); - rpc BrainMessages (Pubkey) returns (stream BrainMessage); + rpc BrainMessages (DaemonStreamAuth) returns (stream BrainMessage); rpc DaemonMessages (stream DaemonMessage) returns (Empty); } diff --git a/src/data.rs b/src/data.rs index c8f9f79..d286c52 100644 --- a/src/data.rs +++ b/src/data.rs @@ -204,16 +204,11 @@ impl BrainData { .clone(); let minutes_to_collect = (Utc::now() - c.collected_at).num_minutes() as u64; c.collected_at = Utc::now(); - log::debug!("{minutes_to_collect}"); - let mut nanolp_to_collect = - c.price_per_minute().saturating_mul(minutes_to_collect); + let mut nanolp_to_collect = c.price_per_minute().saturating_mul(minutes_to_collect); if nanolp_to_collect > c.locked_nano { nanolp_to_collect = c.locked_nano; } - log::debug!( - "Removing {nanolp_to_collect} nanoLP from {}", - c.uuid - ); + log::debug!("Removing {nanolp_to_collect} nanoLP from {}", c.uuid); c.locked_nano -= nanolp_to_collect; self.add_nano_to_wallet(&owner_key, nanolp_to_collect); if c.locked_nano == 0 { @@ -228,6 +223,7 @@ impl BrainData { let msg = grpc::BrainMessage { msg: Some(grpc::brain_message::Msg::DeleteVm(grpc::DeleteVmReq { uuid: uuid.to_string(), + admin_pubkey: String::new(), })), }; let daemon_tx = daemon_tx.clone(); @@ -282,13 +278,13 @@ impl BrainData { pub fn extend_contract_time( &self, uuid: &str, - account: &str, + wallet: &str, nano_lp: u64, ) -> Result<(), Error> { if nano_lp > 100_000_000_000_000 { return Err(Error::TxTooBig); } - let mut account = match self.accounts.get_mut(account) { + let mut account = match self.accounts.get_mut(wallet) { Some(account) => account, None => return Err(Error::InsufficientFunds), }; @@ -300,6 +296,9 @@ impl BrainData { .find(|c| c.uuid == uuid) { Some(contract) => { + if contract.admin_pubkey != wallet { + return Err(Error::ContractNotFound(uuid.to_string())); + } if account.balance + contract.locked_nano < nano_lp { return Err(Error::InsufficientFunds); } @@ -344,31 +343,41 @@ impl BrainData { self.daemon_tx.remove(node_pubkey); } - pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) { - if let Some(contract) = self.find_contract_by_uuid(&delete_vm.uuid) { - info!("Found vm {}. Deleting...", delete_vm.uuid); - if let Some(daemon_tx) = self.daemon_tx.get(&contract.node_pubkey) { - debug!( - "TX for daemon {} found. Informing daemon about deletion of {}.", - contract.node_pubkey, delete_vm.uuid - ); - let msg = grpc::BrainMessage { - msg: Some(grpc::brain_message::Msg::DeleteVm(delete_vm.clone())), - }; - if let Err(e) = daemon_tx.send(msg).await { - warn!( - "Failed to send deletion request to {} due to error: {e:?}", - contract.node_pubkey - ); - info!("Deleting daemon TX for {}", contract.node_pubkey); - self.del_daemon_tx(&contract.node_pubkey); + pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) -> Result<(), Error> { + let contract = match self.find_contract_by_uuid(&delete_vm.uuid) { + Some(contract) => { + if contract.admin_pubkey != delete_vm.admin_pubkey { + return Err(Error::ContractNotFound(delete_vm.uuid)); } + contract + } + None => { + return Err(Error::ContractNotFound(delete_vm.uuid)); + } + }; + info!("Found vm {}. Deleting...", delete_vm.uuid); + if let Some(daemon_tx) = self.daemon_tx.get(&contract.node_pubkey) { + debug!( + "TX for daemon {} found. Informing daemon about deletion of {}.", + contract.node_pubkey, delete_vm.uuid + ); + let msg = grpc::BrainMessage { + msg: Some(grpc::brain_message::Msg::DeleteVm(delete_vm.clone())), + }; + if let Err(e) = daemon_tx.send(msg).await { + warn!( + "Failed to send deletion request to {} due to error: {e:?}", + contract.node_pubkey + ); + info!("Deleting daemon TX for {}", contract.node_pubkey); + self.del_daemon_tx(&contract.node_pubkey); } - - self.add_nano_to_wallet(&contract.admin_pubkey, contract.locked_nano); - let mut contracts = self.contracts.write().unwrap(); - contracts.retain(|c| c.uuid != delete_vm.uuid); } + + self.add_nano_to_wallet(&contract.admin_pubkey, contract.locked_nano); + let mut contracts = self.contracts.write().unwrap(); + contracts.retain(|c| c.uuid != delete_vm.uuid); + Ok(()) } pub async fn submit_newvm_resp(&self, new_vm_resp: grpc::NewVmResp) { @@ -546,7 +555,17 @@ impl BrainData { let uuid = req.uuid.clone(); info!("Inserting new vm update request in memory: {req:?}"); let node_pubkey = match self.find_contract_by_uuid(&req.uuid) { - Some(contract) => contract.node_pubkey, + Some(contract) => { + if contract.admin_pubkey != req.admin_pubkey { + let _ = tx.send(grpc::UpdateVmResp { + uuid, + error: "Contract does not exist.".to_string(), + args: None, + }); + return; + } + contract.node_pubkey + } None => { log::warn!( "Received UpdateVMReq for a contract that does not exist: {}", diff --git a/src/grpc.rs b/src/grpc.rs index e2e9055..6d3d102 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -42,7 +42,7 @@ impl BrainDaemon for BrainDaemonMock { &self, req: Request, ) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("Starting registration process for {:?}", req); let node = crate::data::Node { public_key: req.node_pubkey.clone(), @@ -73,12 +73,19 @@ impl BrainDaemon for BrainDaemonMock { type BrainMessagesStream = Pin> + Send>>; async fn brain_messages( &self, - req: Request, + req: Request, ) -> Result, Status> { - let req = req.into_inner(); - info!("Daemon {} connected to receive brain messages", req.pubkey); + let auth = req.into_inner(); + let pubkey = auth.pubkey.clone(); + check_sig_from_parts( + &pubkey, + &auth.timestamp, + &format!("{:?}", auth.contracts), + &auth.signature, + )?; + info!("Daemon {} connected to receive brain messages", pubkey); let (tx, rx) = mpsc::channel(6); - self.data.add_daemon_tx(&req.pubkey, tx); + self.data.add_daemon_tx(&pubkey, tx); let output_stream = ReceiverStream::new(rx).map(|msg| Ok(msg)); Ok(Response::new( Box::pin(output_stream) as Self::BrainMessagesStream @@ -90,14 +97,30 @@ impl BrainDaemon for BrainDaemonMock { req: Request>, ) -> Result, Status> { let mut req_stream = req.into_inner(); - let mut pubkey = String::new(); + let pubkey: String; + if let Some(Ok(msg)) = req_stream.next().await { + log::debug!("demon_messages received the following auth message: {:?}", msg.msg); + if let Some(daemon_message::Msg::Auth(auth)) = msg.msg { + pubkey = auth.pubkey.clone(); + check_sig_from_parts( + &pubkey, + &auth.timestamp, + &format!("{:?}", auth.contracts), + &auth.signature, + )?; + } else { + return Err(Status::unauthenticated( + "Could not authenticate the daemon: could not extract auth signature", + )); + } + } else { + return Err(Status::unauthenticated("Could not authenticate the daemon")); + } + + // info!("Received a message from daemon {pubkey}: {daemon_message:?}"); while let Some(daemon_message) = req_stream.next().await { - info!("Received a message from daemon {pubkey}: {daemon_message:?}"); match daemon_message { Ok(msg) => match msg.msg { - Some(daemon_message::Msg::Pubkey(p)) => { - pubkey = p.pubkey; - } Some(daemon_message::Msg::NewVmResp(new_vm_resp)) => { self.data.submit_newvm_resp(new_vm_resp).await; } @@ -107,7 +130,7 @@ impl BrainDaemon for BrainDaemonMock { Some(daemon_message::Msg::NodeResources(node_resources)) => { self.data.submit_node_resources(node_resources); } - None => {} + _ => {} }, Err(e) => { log::warn!("Daemon disconnected: {e:?}"); @@ -122,18 +145,18 @@ impl BrainDaemon for BrainDaemonMock { #[tonic::async_trait] impl BrainCli for BrainCliMock { async fn get_balance(&self, req: Request) -> Result, Status> { - Ok(Response::new( - self.data.get_balance(&req.into_inner().pubkey).into(), - )) + let req = check_sig_from_req(req)?; + Ok(Response::new(self.data.get_balance(&req.pubkey).into())) } async fn get_airdrop(&self, req: Request) -> Result, Status> { - self.data.get_airdrop(&req.into_inner().pubkey); + let req = check_sig_from_req(req)?; + self.data.get_airdrop(&req.pubkey); Ok(Response::new(Empty {})) } async fn new_vm(&self, req: Request) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("New VM requested via CLI: {req:?}"); let admin_pubkey = req.admin_pubkey.clone(); let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); @@ -153,7 +176,7 @@ impl BrainCli for BrainCliMock { } async fn update_vm(&self, req: Request) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("Update VM requested via CLI: {req:?}"); let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); self.data.submit_updatevm_req(req, oneshot_tx).await; @@ -169,7 +192,7 @@ impl BrainCli for BrainCliMock { } async fn extend_vm(&self, req: Request) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; match self .data .extend_contract_time(&req.uuid, &req.admin_pubkey, req.locked_nano) @@ -184,7 +207,7 @@ impl BrainCli for BrainCliMock { &self, req: Request, ) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("CLI {} requested ListVMContractsStream", req.admin_pubkey); let contracts = match req.uuid.is_empty() { false => match self.data.find_contract_by_uuid(&req.uuid) { @@ -210,7 +233,7 @@ impl BrainCli for BrainCliMock { &self, req: Request, ) -> Result, tonic::Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("Unknown CLI requested ListNodesStream: {req:?}"); let nodes = self.data.find_nodes_by_filters(&req); let (tx, rx) = mpsc::channel(6); @@ -229,7 +252,7 @@ impl BrainCli for BrainCliMock { &self, req: Request, ) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("Unknown CLI requested ListNodesStream: {req:?}"); match self.data.get_one_node_by_filters(&req) { Some(node) => Ok(Response::new(node.into())), @@ -240,9 +263,166 @@ impl BrainCli for BrainCliMock { } async fn delete_vm(&self, req: Request) -> Result, Status> { - let req = req.into_inner(); + let req = check_sig_from_req(req)?; info!("Unknown CLI requested to delete vm {}", req.uuid); - self.data.delete_vm(req).await; - Ok(Response::new(Empty {})) + match self.data.delete_vm(req).await { + Ok(()) => Ok(Response::new(Empty {})), + Err(e) => Err(Status::not_found(e.to_string())), + } } } + +trait PubkeyGetter { + fn get_pubkey(&self) -> Option; +} + +impl PubkeyGetter for Pubkey { + fn get_pubkey(&self) -> Option { + Some(self.pubkey.clone()) + } +} + +impl PubkeyGetter for NewVmReq { + fn get_pubkey(&self) -> Option { + Some(self.admin_pubkey.clone()) + } +} + +impl PubkeyGetter for DeleteVmReq { + fn get_pubkey(&self) -> Option { + Some(self.admin_pubkey.clone()) + } +} + +impl PubkeyGetter for UpdateVmReq { + fn get_pubkey(&self) -> Option { + Some(self.admin_pubkey.clone()) + } +} + +impl PubkeyGetter for ExtendVmReq { + fn get_pubkey(&self) -> Option { + Some(self.admin_pubkey.clone()) + } +} + +impl PubkeyGetter for ListContractsReq { + fn get_pubkey(&self) -> Option { + Some(self.admin_pubkey.clone()) + } +} + +impl PubkeyGetter for NodeFilters { + fn get_pubkey(&self) -> Option { + None + } +} + +impl PubkeyGetter for RegisterNodeReq { + fn get_pubkey(&self) -> Option { + Some(self.node_pubkey.clone()) + } +} + +fn check_sig_from_req(req: Request) -> Result { + let time = match req.metadata().get("timestamp") { + Some(t) => t.clone(), + None => return Err(Status::unauthenticated("Timestamp not found in metadata.")), + }; + let time = time + .to_str() + .map_err(|_| Status::unauthenticated("Timestamp in metadata is not a string"))?; + + let now = chrono::Utc::now(); + let parsed_time = chrono::DateTime::parse_from_rfc3339(time) + .map_err(|_| Status::unauthenticated("Coult not parse timestamp"))?; + let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds(); + if seconds_elapsed > 1 || seconds_elapsed < -1 { + return Err(Status::unauthenticated(format!( + "Date is not within 1 sec of the time of the server: CLI {} vs Server {}", + parsed_time, now + ))); + } + + let signature = match req.metadata().get("request-signature") { + Some(t) => t, + None => return Err(Status::unauthenticated("signature not found in metadata.")), + }; + let signature = bs58::decode(signature) + .into_vec() + .map_err(|_| Status::unauthenticated("signature is not a bs58 string"))?; + let signature = ed25519_dalek::Signature::from_bytes( + signature + .as_slice() + .try_into() + .map_err(|_| Status::unauthenticated("could not parse ed25519 signature"))?, + ); + + let pubkey_value = match req.metadata().get("pubkey") { + Some(p) => p.clone(), + None => return Err(Status::unauthenticated("Signature not found in metadata.")), + }; + let pubkey = ed25519_dalek::VerifyingKey::from_bytes( + &bs58::decode(&pubkey_value) + .into_vec() + .map_err(|_| Status::unauthenticated("pubkey is not a bs58 string"))? + .try_into() + .map_err(|_| Status::unauthenticated("pubkey does not have the correct size."))?, + ) + .map_err(|_| Status::unauthenticated("could not parse ed25519 pubkey"))?; + + let req = req.into_inner(); + let message = format!("{time}{req:?}"); + use ed25519_dalek::Verifier; + pubkey + .verify(message.as_bytes(), &signature) + .map_err(|_| Status::unauthenticated("the signature is not valid"))?; + if let Some(req_pubkey) = req.get_pubkey() { + if pubkey_value.to_str().unwrap().to_string() != req_pubkey { + return Err(Status::unauthenticated( + "pubkey of signature does not match pubkey of request", + )); + } + } + Ok(req) +} + +fn check_sig_from_parts(pubkey: &str, time: &str, msg: &str, sig: &str) -> Result<(), Status> { + let now = chrono::Utc::now(); + let parsed_time = chrono::DateTime::parse_from_rfc3339(time) + .map_err(|_| Status::unauthenticated("Coult not parse timestamp"))?; + let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds(); + if seconds_elapsed > 1 || seconds_elapsed < -1 { + return Err(Status::unauthenticated(format!( + "Date is not within 1 sec of the time of the server: CLI {} vs Server {}", + parsed_time, now + ))); + } + + let signature = bs58::decode(sig) + .into_vec() + .map_err(|_| Status::unauthenticated("signature is not a bs58 string"))?; + let signature = ed25519_dalek::Signature::from_bytes( + signature + .as_slice() + .try_into() + .map_err(|_| Status::unauthenticated("could not parse ed25519 signature"))?, + ); + + let pubkey = ed25519_dalek::VerifyingKey::from_bytes( + &bs58::decode(&pubkey) + .into_vec() + .map_err(|_| Status::unauthenticated("pubkey is not a bs58 string"))? + .try_into() + .map_err(|_| Status::unauthenticated("pubkey does not have the correct size."))?, + ) + .map_err(|_| Status::unauthenticated("could not parse ed25519 pubkey"))?; + + let msg = time.to_string() + msg; + use ed25519_dalek::Verifier; + pubkey + .verify(msg.as_bytes(), &signature) + .map_err(|_| Status::unauthenticated("the signature is not valid"))?; + + Ok(()) +}