diff --git a/src/config/schema.rs b/src/config/schema.rs index 4c81324..1912334 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -763,7 +763,8 @@ pub struct WhatsAppConfig { pub phone_number_id: String, /// Webhook verify token (you define this, Meta sends it back for verification) pub verify_token: String, - /// App secret for webhook signature verification (X-Hub-Signature-256) + /// App secret from Meta Business Suite (for webhook signature verification) + /// Can also be set via `ZEROCLAW_WHATSAPP_APP_SECRET` environment variable #[serde(default)] pub app_secret: Option, /// Allowed phone numbers (E.164 format: +1234567890) or "*" for all @@ -1488,7 +1489,7 @@ channel_id = "C123" access_token: "tok".into(), phone_number_id: "12345".into(), verify_token: "verify".into(), - app_secret: None, + app_secret: Some("secret123".into()), allowed_numbers: vec!["+1".into()], }; let toml_str = toml::to_string(&wc).unwrap(); diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index 55753a1..3a74a50 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -1700,8 +1700,8 @@ fn setup_channels() -> Result { access_token: access_token.trim().to_string(), phone_number_id: phone_number_id.trim().to_string(), verify_token: verify_token.trim().to_string(), - allowed_numbers, app_secret: None, // Can be set via ZEROCLAW_WHATSAPP_APP_SECRET env var + allowed_numbers, }); } 6 => { diff --git a/tests/whatsapp_webhook_security.rs b/tests/whatsapp_webhook_security.rs new file mode 100644 index 0000000..c9f03f2 --- /dev/null +++ b/tests/whatsapp_webhook_security.rs @@ -0,0 +1,129 @@ +//! Integration tests for WhatsApp webhook signature verification. +//! +//! These tests validate that: +//! 1. Webhooks with valid signatures are accepted +//! 2. Webhooks with invalid signatures are rejected +//! 3. Webhooks with missing signatures are rejected +//! 4. Webhooks are rejected even if JSON is valid but signature is bad + +use hmac::{Hmac, Mac}; +use sha2::Sha256; + +/// Compute valid HMAC-SHA256 signature for a webhook payload +fn compute_signature(app_secret: &str, body: &[u8]) -> String { + let mut mac = Hmac::::new_from_slice(app_secret.as_bytes()).unwrap(); + mac.update(body); + let result = mac.finalize(); + format!("sha256={}", hex::encode(result.into_bytes())) +} + +#[test] +fn whatsapp_signature_rejects_missing_sha256_prefix() { + let secret = "test_app_secret"; + let body = b"test payload"; + let bad_sig = "abc123"; // Missing sha256= prefix + + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret, body, bad_sig + )); +} + +#[test] +fn whatsapp_signature_rejects_invalid_hex() { + let secret = "test_app_secret"; + let body = b"test payload"; + let bad_sig = "sha256=not-valid-hex!!"; + + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret, body, bad_sig + )); +} + +#[test] +fn whatsapp_signature_rejects_wrong_signature() { + let secret = "test_app_secret"; + let body = b"test payload"; + let bad_sig = "sha256=00112233445566778899aabbccddeeff"; + + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret, body, bad_sig + )); +} + +#[test] +fn whatsapp_signature_accepts_valid_signature() { + let secret = "test_app_secret"; + let body = b"test payload"; + let valid_sig = compute_signature(secret, body); + + assert!(zeroclaw::gateway::verify_whatsapp_signature( + secret, body, &valid_sig + )); +} + +#[test] +fn whatsapp_signature_rejects_tampered_body() { + let secret = "test_app_secret"; + let original_body = b"original message"; + let tampered_body = b"tampered message"; + + // Compute signature for original body + let sig = compute_signature(secret, original_body); + + // Tampered body should be rejected even with valid-looking signature + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret, tampered_body, &sig + )); +} + +#[test] +fn whatsapp_signature_rejects_wrong_secret() { + let correct_secret = "correct_secret"; + let wrong_secret = "wrong_secret"; + let body = b"test payload"; + + // Compute signature with correct secret + let sig = compute_signature(correct_secret, body); + + // Wrong secret should reject the signature + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + wrong_secret, body, &sig + )); +} + +#[test] +fn whatsapp_signature_rejects_empty_signature() { + let secret = "test_app_secret"; + let body = b"test payload"; + + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret, body, "" + )); +} + +#[test] +fn whatsapp_signature_different_secrets_produce_different_sigs() { + let secret1 = "secret_one"; + let secret2 = "secret_two"; + let body = b"same payload"; + + let sig1 = compute_signature(secret1, body); + let sig2 = compute_signature(secret2, body); + + // Different secrets should produce different signatures + assert_ne!(sig1, sig2); + + // Each signature should only verify with its own secret + assert!(zeroclaw::gateway::verify_whatsapp_signature( + secret1, body, &sig1 + )); + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret2, body, &sig1 + )); + assert!(zeroclaw::gateway::verify_whatsapp_signature( + secret2, body, &sig2 + )); + assert!(!zeroclaw::gateway::verify_whatsapp_signature( + secret1, body, &sig2 + )); +}