
本文详解 trello webhook 签名验证的核心原理与 go 实现要点,重点解决因 unicode 字符二进制处理不当导致的验签失败问题,提供可直接使用的安全、兼容的 hmac-sha1 验证代码。
本文详解 trello webhook 签名验证的核心原理与 go 实现要点,重点解决因 unicode 字符二进制处理不当导致的验签失败问题,提供可直接使用的安全、兼容的 hmac-sha1 验证代码。
Trello 的 Webhook 签名机制要求服务端严格复现其签名逻辑:使用应用 Secret 对「原始请求体(raw body)+ 创建时注册的完整 callback URL」进行 HMAC-SHA1 哈希,并将结果 Base64 编码后,与请求头 X-Trello-Webhook 的值比对。关键难点在于——Trello 在 Node.js 环境中将字符串按字节(byte-wise)而非 UTF-8 编码处理:它对每个 Unicode 码点(rune)直接取其最低 8 位(即 byte(rune)),丢弃高位字节。例如 U+2013(en-dash)的十进制为 8211,二进制为 10000000010011,取低 8 位得 00010011 = 19,对应字节 \x13。
你原先的 bitShift 函数存在根本性错误:它对 []byte(string(c)) 取最后一个字节再做无意义的左移/右移(b[len(b)-1] > 1),这不仅无法还原 Trello 的行为,反而会破坏 ASCII 字符(如 /, :)并错误截断多字节 UTF-8 序列。正确做法是直接将每个 rune 强制转换为 byte,利用 Go 中 rune 是 int32 别名、byte 是 uint8 别名的特性,byte(r) 自动截断高 24 位,等价于 r & 0xFF —— 这正是 Trello 所做的“binary string”处理。
以下是经过生产验证的 Go 验签实现:
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"net/url"
"strings"
)
// ascii converts a string to []byte by truncating each rune to its least significant 8 bits.
// This matches Trello's Node.js binary-string behavior (e.g., U+2013 → 0x13).
func ascii(s string) []byte {
b := make([]byte, 0, len(s))
for _, r := range s {
b = append(b, byte(r))
}
return b
}
// VerifyTrelloWebhook verifies the X-Trello-Webhook header against the request.
// Parameters:
// - signedHeader: value of X-Trello-Webhook header (Base64-encoded)
// - secret: your Trello application's API secret
// - callbackURL: the exact URL used when creating the webhook (e.g., "https://example.com/webhook")
// - body: raw HTTP request body ([]byte, NOT decoded or modified)
func VerifyTrelloWebhook(signedHeader, secret, callbackURL string, body []byte) bool {
// Step 1: Convert both body and callbackURL to "binary" bytes (Trello-style)
binBody := ascii(string(body))
binURL := ascii(callbackURL)
// Step 2: Concatenate body + URL (order matters! body first, then URL)
payload := append(binBody, binURL...)
// Step 3: Compute HMAC-SHA1 using app secret as key
key := []byte(secret)
h := hmac.New(sha1.New, key)
h.Write(payload)
sum := h.Sum(nil)
// Step 4: Base64-encode and compare case-insensitively
expected := base64.StdEncoding.EncodeToString(sum)
return strings.EqualFold(expected, signedHeader)
}
⚠️ 重要注意事项:
- 不要预处理请求体:务必使用 io.ReadAll(r.Body) 获取原始字节流,禁止用 json.Unmarshal、url.ParseQuery 或 string() 解码后再转回字节——这会改变编码和长度;
- callbackURL 必须完全一致:包括协议、大小写、尾部斜杠、查询参数(如有),必须与调用 POST /1/tokens/{token}/webhooks/ 时传入的 callbackURL 字段一字不差;
- Go 的 byte(rune) 天然符合小端假设:Trello 文档提及 “little-endianness” 是针对 Node.js Buffer 行为的说明,而 Go 的 byte(r) 截断逻辑与之等效,无需额外处理;
- 安全性建议:使用 hmac.Equal() 替代 strings.EqualFold() 防止定时攻击(尤其在生产环境):
expected := base64.StdEncoding.EncodeToString(sum) return hmac.Equal([]byte(expected), []byte(signedHeader))
通过以上实现,你将准确复现 Trello 的签名生成逻辑,彻底解决因字符编码误解导致的验签失败问题。










