diff --git a/Cargo.lock b/Cargo.lock index b5838c9fdb..0c1df81c04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1799,33 +1799,6 @@ dependencies = [ "syn 2.0.99", ] -[[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 2.0.99", -] - [[package]] name = "darling" version = "0.20.10" @@ -2129,30 +2102,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" -[[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.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - [[package]] name = "either" version = "1.15.0" @@ -2344,12 +2293,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "filetime" version = "0.2.25" @@ -2929,7 +2872,6 @@ dependencies = [ "chrono", "clap", "config", - "ed25519-dalek", "futures", "goose", "goose-mcp", diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 698e9694e1..182c19e155 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -44,7 +44,6 @@ url = "2.5.7" rand = "0.9.2" hex = "0.4.3" socket2 = "0.6.1" -ed25519-dalek = "2.1" sha2 = "0.10.9" [target.'cfg(windows)'.dependencies] diff --git a/crates/goose-server/src/tunnel/ed25519.rs b/crates/goose-server/src/tunnel/ed25519.rs deleted file mode 100644 index f45be66d15..0000000000 --- a/crates/goose-server/src/tunnel/ed25519.rs +++ /dev/null @@ -1,176 +0,0 @@ -use anyhow::Result; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; -use sha2::{Digest, Sha256}; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub struct Ed25519Validator { - public_key: VerifyingKey, -} - -impl Ed25519Validator { - pub fn new(public_key_hex: &str) -> Result { - let bytes = hex::decode(public_key_hex)?; - if bytes.len() != 32 { - anyhow::bail!("Public key must be 32 bytes"); - } - let public_key = - VerifyingKey::from_bytes(&bytes.try_into().expect("verified 32 bytes above"))?; - Ok(Self { public_key }) - } - - pub fn verify( - &self, - signature_header: &str, - method: &str, - path: &str, - body: Option<&str>, - ) -> Result<()> { - let parts: Vec<&str> = signature_header.split('.').collect(); - if parts.len() != 2 { - anyhow::bail!("Invalid signature format"); - } - - let timestamp = parts[0]; - let sig_hex = parts[1]; - - let timestamp_secs: i64 = timestamp - .parse() - .map_err(|_| anyhow::anyhow!("Invalid timestamp format"))?; - let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let age = now - timestamp_secs; - - const MAX_AGE_SECS: i64 = 300; - if age > MAX_AGE_SECS { - anyhow::bail!("Signature expired (age: {}s)", age); - } - if age < -MAX_AGE_SECS { - anyhow::bail!("Signature timestamp too far in future"); - } - - let body_hash = if let Some(body_content) = body { - let hash = Sha256::digest(body_content.as_bytes()); - hex::encode(hash) - } else { - String::new() - }; - - let message = format!("{}|{}|{}|{}", method, path, timestamp, body_hash); - let sig_bytes = hex::decode(sig_hex)?; - let signature = Signature::from_bytes( - &sig_bytes - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid signature length"))?, - ); - - self.public_key.verify(message.as_bytes(), &signature)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ed25519_dalek::{Signer, SigningKey}; - - #[test] - #[allow(clippy::zero_prefixed_literal)] - fn test_signature_validation() { - let signing_key = SigningKey::from_bytes(&[ - 157, 097, 177, 157, 239, 253, 090, 096, 186, 132, 074, 244, 146, 236, 044, 196, 068, - 073, 197, 105, 123, 050, 105, 025, 112, 059, 172, 003, 028, 174, 127, 096, - ]); - let verifying_key = signing_key.verifying_key(); - let public_key_hex = hex::encode(verifying_key.as_bytes()); - - let validator = Ed25519Validator::new(&public_key_hex).unwrap(); - - let method = "POST"; - let path = "/api/test"; - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - .to_string(); - let body = r#"{"test":"data"}"#; - - let body_hash = hex::encode(Sha256::digest(body.as_bytes())); - let message = format!("{}|{}|{}|{}", method, path, timestamp, body_hash); - let signature = signing_key.sign(message.as_bytes()); - let sig_hex = hex::encode(signature.to_bytes()); - - let signature_header = format!("{}.{}", timestamp, sig_hex); - - assert!(validator - .verify(&signature_header, method, path, Some(body)) - .is_ok()); - } - - #[test] - fn test_invalid_signature() { - let signing_key = SigningKey::from_bytes(&[1u8; 32]); - let verifying_key = signing_key.verifying_key(); - let public_key_hex = hex::encode(verifying_key.as_bytes()); - - let validator = Ed25519Validator::new(&public_key_hex).unwrap(); - - let signature_header = "1731384000.deadbeef"; - assert!(validator - .verify(signature_header, "GET", "/test", None) - .is_err()); - } - - #[test] - fn test_expired_signature() { - let signing_key = SigningKey::from_bytes(&[1u8; 32]); - let verifying_key = signing_key.verifying_key(); - let public_key_hex = hex::encode(verifying_key.as_bytes()); - - let validator = Ed25519Validator::new(&public_key_hex).unwrap(); - - let method = "GET"; - let path = "/test"; - let old_timestamp = "1731384000"; - - let message = format!("{}|{}|{}|", method, path, old_timestamp); - let signature = signing_key.sign(message.as_bytes()); - let sig_hex = hex::encode(signature.to_bytes()); - let signature_header = format!("{}.{}", old_timestamp, sig_hex); - - let result = validator.verify(&signature_header, method, path, None); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Signature expired")); - } - - #[test] - fn test_future_timestamp() { - let signing_key = SigningKey::from_bytes(&[1u8; 32]); - let verifying_key = signing_key.verifying_key(); - let public_key_hex = hex::encode(verifying_key.as_bytes()); - - let validator = Ed25519Validator::new(&public_key_hex).unwrap(); - - let method = "GET"; - let path = "/test"; - let future_timestamp = (SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - + 400) - .to_string(); - - let message = format!("{}|{}|{}|", method, path, future_timestamp); - let signature = signing_key.sign(message.as_bytes()); - let sig_hex = hex::encode(signature.to_bytes()); - let signature_header = format!("{}.{}", future_timestamp, sig_hex); - - let result = validator.verify(&signature_header, method, path, None); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("too far in future")); - } -} diff --git a/crates/goose-server/src/tunnel/lapstone.rs b/crates/goose-server/src/tunnel/lapstone.rs index fbc4f1aa6d..3172b5966e 100644 --- a/crates/goose-server/src/tunnel/lapstone.rs +++ b/crates/goose-server/src/tunnel/lapstone.rs @@ -111,33 +111,6 @@ fn validate_and_build_request( anyhow::bail!("Invalid tunnel secret"); } - // Ed25519 signature validation (if configured) - if let Ok(public_key_hex) = std::env::var("GOOSE_TUNNEL_PUBLIC_KEY_AUTH") { - match super::ed25519::Ed25519Validator::new(&public_key_hex) { - Ok(validator) => { - let sig_header = message - .headers - .as_ref() - .and_then(|h| { - h.iter() - .find(|(k, _)| k.eq_ignore_ascii_case("x-corp-signature")) - .map(|(_, v)| v) - }) - .ok_or_else(|| anyhow::anyhow!("Missing X-Corp-Signature header"))?; - - validator.verify( - sig_header, - &message.method, - &message.path, - message.body.as_deref(), - )?; - } - Err(e) => { - anyhow::bail!("Failed to create Ed25519 validator: {}", e); - } - } - } - let mut request_builder = match message.method.as_str() { "GET" => client.get(url), "POST" => client.post(url), @@ -149,7 +122,7 @@ fn validate_and_build_request( if let Some(headers) = &message.headers { for (key, value) in headers { - if key.to_lowercase() == "x-secret-key" { + if key.eq_ignore_ascii_case("x-secret-key") { continue; } request_builder = request_builder.header(key, value); diff --git a/crates/goose-server/src/tunnel/mod.rs b/crates/goose-server/src/tunnel/mod.rs index 7024574249..46c1d05a38 100644 --- a/crates/goose-server/src/tunnel/mod.rs +++ b/crates/goose-server/src/tunnel/mod.rs @@ -1,4 +1,3 @@ -pub mod ed25519; pub mod lapstone; #[cfg(test)] @@ -124,7 +123,7 @@ impl TunnelManager { } } - pub async fn update_config(&self, f: F) + pub async fn update_config(&self, f: F) -> anyhow::Result<()> where F: FnOnce(&mut TunnelConfig), { @@ -132,21 +131,21 @@ impl TunnelManager { f(&mut cfg); let global_cfg = Config::global(); - if let Err(e) = global_cfg.set_param("tunnel_auto_start", cfg.auto_start) { - tracing::error!("Failed to save tunnel config: {}", e); - return; - } + global_cfg + .set_param("tunnel_auto_start", cfg.auto_start) + .map_err(|e| anyhow::anyhow!("Failed to save tunnel config: {}", e))?; if let Some(secret) = &cfg.secret { - if let Err(e) = global_cfg.set_secret("tunnel_secret", secret) { - tracing::error!("Failed to save tunnel secret: {}", e); - } + global_cfg + .set_secret("tunnel_secret", secret) + .map_err(|e| anyhow::anyhow!("Failed to save tunnel secret: {}", e))?; } if let Some(agent_id) = &cfg.agent_id { - if let Err(e) = global_cfg.set_secret("tunnel_agent_id", agent_id) { - tracing::error!("Failed to save tunnel agent_id: {}", e); - } + global_cfg + .set_secret("tunnel_agent_id", agent_id) + .map_err(|e| anyhow::anyhow!("Failed to save tunnel agent_id: {}", e))?; } + Ok(()) } async fn start_tunnel_internal(&self) -> anyhow::Result<(TunnelInfo, mpsc::Receiver<()>)> { @@ -162,7 +161,7 @@ impl TunnelManager { c.secret = Some(tunnel_secret.clone()); c.agent_id = Some(agent_id.clone()); }) - .await; + .await?; let (restart_tx, restart_rx) = mpsc::channel::<()>(1); *self.restart_tx.write().await = Some(restart_tx.clone()); @@ -195,7 +194,7 @@ impl TunnelManager { Ok((info, mut restart_rx)) => { *self.state.write().await = TunnelState::Running; *self.info.write().await = Some(info.clone()); - self.update_config(|c| c.auto_start = true).await; + let _ = self.update_config(|c| c.auto_start = true).await; let state = self.state.clone(); let config = self.config.clone(); @@ -267,7 +266,7 @@ impl TunnelManager { *self.info.write().await = None; if clear_auto_start { - self.update_config(|c| c.auto_start = false).await; + let _ = self.update_config(|c| c.auto_start = false).await; } } }