You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python转TypeScript的Close Webhook签名验证代码为何不等效?

Close Webhook签名验证:Python转TypeScript后验证失败问题

我需要验证Close Webhook的签名,官方仅提供Python版本的示例代码,我将其转换为TypeScript后,相同数据下Python验证通过,但TypeScript验证失败,求问两段代码为何不等效?

官方Python签名验证代码

import hmac
import hashlib
import json


def verify_signature(key, timestamp, provided_signature, payload):
  key_bytes = bytes.fromhex(key)
  payload_str = json.dumps(payload)
  data = timestamp + payload_str
  signature = hmac.new(key_bytes, data.encode('utf-8'),
                       hashlib.sha256).hexdigest()
  valid = hmac.compare_digest(provided_signature, signature)
  return valid

转换后的TypeScript代码

export function verifyCloseSignature(
  request: Request,
  key: string,
  payload: any,
) {
  const headers = request.headers;

  const timestamp = headers.get('close-sig-timestamp');
  const providedSignature = headers.get('close-sig-hash');

  if (!timestamp) {
    throw new Error('[verifyCloseSignature] Required timestamp header missing');
  }

  if (!providedSignature) {
    throw new Error('[verifyCloseSignature] Required signature header missing');
  }

  const payloadString = JSON.stringify(payload);
  const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex'));
  hmac.update(timestamp + payloadString);
  const calculatedSignature = hmac.digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(providedSignature, 'hex'),
    Buffer.from(calculatedSignature, 'hex'),
  );
}

测试逻辑

const headers = new Headers();
headers.set('close-sig-hash', signature);
headers.set('close-sig-timestamp', timestamp.toString());
headers.set('Content-Type', 'application/json');
const request = new Request(faker.internet.url(), {
  method: 'POST',
  headers: headers,
  body: JSON.stringify(payload),
});

const actual = verifyCloseSignature(request, key, payload);
const expected = true;

expect(actual).toEqual(expected);

核心差异:JSON序列化格式不一致

两段代码最关键的区别在于JSON序列化的输出格式:

  • Python的json.dumps()默认使用(', ', ': ')作为分隔符,生成的JSON字符串会在冒号后、逗号后添加空格,例如{"key": "value", "list": [1, 2, 3]}。
  • TypeScript的JSON.stringify()默认使用(',', ':')作为分隔符,生成的JSON字符串没有额外空格,例如{"key":"value","list":[1,2,3]}。

这种格式差异会导致timestamp + payload_str的最终字符串完全不同,进而使得HMAC-SHA256计算出的签名不一致,最终验证失败。

解决方法

要让TypeScript代码和Python代码等效,需要让TypeScript生成与Pythonjson.dumps()默认格式完全一致的JSON字符串。有两种可行方案:

方案1:修改TypeScript的JSON序列化逻辑

由于JSON.stringify()没有直接设置分隔符的参数,你可以对其结果进行后处理,添加必要的空格(适用于基础JSON结构,复杂嵌套需更严谨的处理):

// 生成匹配Python默认格式的JSON字符串
function pythonStyleJsonStringify(obj: any): string {
  return JSON.stringify(obj)
    .replace(/:/g, ': ')
    .replace(/,/g, ', ');
}

// 在验证函数中替换原有的JSON.stringify调用
const payloadString = pythonStyleJsonStringify(payload);

方案2:修改Python代码的序列化逻辑(如果允许)

如果可以调整Python测试代码的序列化方式,让它生成与TypeScript默认格式一致的无空格JSON字符串:

# 修改json.dumps使用无空格分隔符
payload_str = json.dumps(payload, separators=(',', ':'))

生产环境优化建议

如果是验证真实Webhook请求,建议直接使用请求原始的body内容,而非重新序列化传入的payload对象——避免因对象解析/序列化过程中丢失格式信息导致的签名不匹配。例如在TypeScript中直接读取请求的原始body:

export async function verifyCloseSignature(
  request: Request,
  key: string,
) {
  const headers = request.headers;

  const timestamp = headers.get('close-sig-timestamp');
  const providedSignature = headers.get('close-sig-hash');

  if (!timestamp) {
    throw new Error('[verifyCloseSignature] Required timestamp header missing');
  }

  if (!providedSignature) {
    throw new Error('[verifyCloseSignature] Required signature header missing');
  }

  // 读取原始请求body
  const payloadString = await request.text();
  const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex'));
  hmac.update(timestamp + payloadString);
  const calculatedSignature = hmac.digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(providedSignature, 'hex'),
    Buffer.from(calculatedSignature, 'hex'),
  );
}

内容的提问来源于stack exchange,提问作者J. Hesters

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 02:54:55