X-ThunderPhone-Signature 请求头中携带 HMAC-SHA256 签名。一次正确实现验证,然后将同一个辅助函数接入每个处理程序。
算法
- 读取原始请求正文——即我们 POST 给您的精确字节。
- 计算
hmac_sha256(secret, body).hexdigest()。 - 与
X-ThunderPhone-Signature进行恒定时间比较。 (简单的字符串比较会泄露时序信息。)
, 和 :,不含空格)、UTF-8。当您的框架仅提供已解析的 JSON 时,您可以使用第二种完全等效的方法:进行规范化重新序列化,然后对其计算 HMAC。
使用哪个密钥?
将密钥存储在您的密钥管理器或环境变量中——切勿提交到代码仓库。
参考实现
以下四种实现均验证原始请求正文:特定框架接入
验证工具调用
当智能体直接调用您的某个 函数工具(该工具具有endpoint)时,请求会在您配置的 endpoint.headers 之外携带两个 ThunderPhone 请求头:
X-ThunderPhone-Call-ID—— 进行中通话的数字 ID。X-ThunderPhone-Signature—— 使用您的组织级 webhook 密钥作为密钥,对精确的请求正文字节计算的 HMAC-SHA256。
verify() 辅助函数无需修改即可使用,但有两点需要注意:
GET/DELETE工具没有正文。 参数通过查询参数传递,签名基于空字节字符串计算——因此使用verify(b"", sig, secret)(Python)或verify(Buffer.alloc(0), sig, secret)(Node)。请勿对查询字符串进行哈希。- 未配置旧版 webhook 的组织没有组织密钥。 在这种情况下,工具调用仅携带
X-ThunderPhone-Call-ID,不包含签名请求头。请配置旧版 webhook(PUT /v1/webhook)以获取签名密钥,或通过endpoint.headers中您自己的请求头对工具调用进行身份验证。
endpoint 的工具,会以 telephony.tool / web.tool 的形式发送到您的组织 webhook)属于普通的已签名 webhook——适用上述标准方法。有关两种请求形式,请参阅函数工具。
常见陷阱
使用默认格式重新序列化
使用默认格式重新序列化
解析请求体后,再使用 JSON 库的默认设置重新导出(在
, / : 后添加空格、按插入顺序排列键)会生成不同的字节,从而导致 HMAC 验证失败。请验证原始请求体——或者如果必须重新序列化,请严格匹配我们的规范格式:键排序、紧凑分隔符、UTF-8。框架自动解析 JSON
框架自动解析 JSON
Express 的
express.json() 中间件会消耗请求体流,导致您丢失原始字节。请仅在 webhook 路由上使用 express.raw(),或者在前置中间件中缓冲原始请求体。NestJS / Koa 也是如此——请查阅它们关于“原始请求体”的文档。非时序安全的比较
非时序安全的比较
JS 中的
expected === signature 或 Python 中的 expected == signature 都是时序可变比较。请分别使用 crypto.timingSafeEqual 或 hmac.compare_digest。性能差异可以忽略不计。工具端点使用了错误的密钥
工具端点使用了错误的密钥
直接调用工具端点时,签名使用的是组织级 webhook 密钥(
GET /v1/webhook)——而不是 /v1/developer/webhook-endpoints 中任何单个端点的密钥。复用相同的 verify() 函数,但请确保在工具路由中传入组织密钥。在 GET/DELETE 工具中对查询字符串进行哈希
在 GET/DELETE 工具中对查询字符串进行哈希
对于没有请求体的工具方法,签名覆盖空字节字符串,从而保持一套通用规则:对原始请求体进行 HMAC,无论其内容是什么。对 URL 或查询字符串进行哈希永远不会匹配。
不在不匹配时返回 401
不在不匹配时返回 401
验证失败时返回 200 会使处理程序成为重放攻击目标。如果验证失败,始终返回非 2xx 响应。
后续步骤
Webhook 概览
投递语义、重试、源 IP 地址。
Webhook 端点
管理多个 URL、轮换密钥。
函数工具
两种工具调用路径及其请求格式。
工具集成
端到端构建完整的工具支持型集成。