feat: Add HMAC-SHA256 webhook signature validation for Slack (#588)

* feat: Add HMAC-SHA256 webhook signature validation for Slack

* review fixes
This commit is contained in:
Nick Pismenkov
2026-03-05 19:27:10 -08:00
committed by GitHub
parent 2d332f12f0
commit 14de4c1b57
9 changed files with 753 additions and 28 deletions
+57 -24
View File
@@ -2397,6 +2397,7 @@ impl ExtensionManager {
let webhook_secret_name = loaded.webhook_secret_name();
let secret_header = loaded.webhook_secret_header().map(|s| s.to_string());
let sig_key_secret_name = loaded.signature_key_secret_name();
let hmac_secret_name = loaded.hmac_secret_name();
// Get webhook secret from secrets store
let webhook_secret = self
@@ -2480,6 +2481,21 @@ impl ExtensionManager {
}
}
}
// Register HMAC signing secret if declared in capabilities
if let Some(hmac_name) = &hmac_secret_name {
match self.secrets.get_decrypted(&self.user_id, hmac_name).await {
Ok(secret) => {
wasm_channel_router
.register_hmac_secret(&channel_name, secret.expose())
.await;
tracing::info!(channel = %channel_name, "Registered HMAC signing secret for hot-activated channel");
}
Err(e) => {
tracing::warn!(channel = %channel_name, error = %e, "HMAC secret not found");
}
}
}
}
// Inject credentials
@@ -2587,19 +2603,30 @@ impl ExtensionManager {
}
};
// Also refresh the webhook secret in the router
// Load capabilities file to get the correct secret name (may be overridden)
let webhook_secret_name = {
let cap_path = self
.wasm_channels_dir
.join(format!("{}.capabilities.json", name));
match tokio::fs::read(&cap_path).await {
Ok(bytes) => crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&bytes)
.map(|f| f.webhook_secret_name())
.unwrap_or_else(|_| format!("{}_webhook_secret", name)),
Err(_) => format!("{}_webhook_secret", name),
}
// Load capabilities file once to extract all secret names
let cap_path = self
.wasm_channels_dir
.join(format!("{}.capabilities.json", name));
let capabilities_file = match tokio::fs::read(&cap_path).await {
Ok(bytes) => crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&bytes).ok(),
Err(_) => None,
};
// Extract all secret names from the capabilities file
let webhook_secret_name = capabilities_file
.as_ref()
.map(|f| f.webhook_secret_name())
.unwrap_or_else(|| format!("{}_webhook_secret", name));
let sig_key_secret_name = capabilities_file
.as_ref()
.and_then(|f| f.signature_key_secret_name().map(|s| s.to_string()));
let hmac_secret_name = capabilities_file
.as_ref()
.and_then(|f| f.hmac_secret_name().map(|s| s.to_string()));
// Refresh webhook secret
if let Ok(secret) = self
.secrets
.get_decrypted(&self.user_id, &webhook_secret_name)
@@ -2618,18 +2645,7 @@ impl ExtensionManager {
existing_channel.update_config(config_updates).await;
}
// Also refresh signature key in the router
let sig_key_secret_name = {
let cap_path = self
.wasm_channels_dir
.join(format!("{}.capabilities.json", name));
match tokio::fs::read(&cap_path).await {
Ok(bytes) => crate::channels::wasm::ChannelCapabilitiesFile::from_bytes(&bytes)
.ok()
.and_then(|f| f.signature_key_secret_name().map(|s| s.to_string())),
Err(_) => None,
}
};
// Refresh signature key
if let Some(ref sig_key_name) = sig_key_secret_name
&& let Ok(key_secret) = self
.secrets
@@ -2649,6 +2665,23 @@ impl ExtensionManager {
}
}
// Refresh HMAC signing secret
if let Some(ref hmac_secret_name_ref) = hmac_secret_name {
match self
.secrets
.get_decrypted(&self.user_id, hmac_secret_name_ref)
.await
{
Ok(secret) => {
router.register_hmac_secret(name, secret.expose()).await;
tracing::info!(channel = %name, "Refreshed HMAC signing secret");
}
Err(e) => {
tracing::warn!(channel = %name, error = %e, "HMAC secret not found");
}
}
}
// Refresh tunnel_url in case it wasn't set at startup
if let Some(ref tunnel_url) = self.tunnel_url {
let mut config_updates = std::collections::HashMap::new();