签名参考
本页是 TEA1-HMAC-SHA256 请求签名方案的精确参考,该方案用于所有客户 API 端点。它包含规范字符串规则、冻结的测试向量,以及 Python、Go、Rust 的参考实现,方便你用已知答案校验自己的客户端。
有关流程讲解与 TypeScript 示例,请参阅 认证。
请求头
每个已签名请求都携带以下五个请求头:
| 请求头 | 取值 |
|---|---|
x-api-key | 你的 access key id(公开)。 |
x-timestamp | 请求创建时的 Unix 时间(秒)。 |
x-nonce | 随机字符串,每个请求唯一(16 个随机字节的十六进制)。 |
x-body-hash | hex(sha256(rawBody)) —— 精确请求体的哈希;空 body 仍要计算(sha256(""))。 |
x-signature | hex(HMAC-SHA256(secret, canonicalString))。 |
规范字符串
签名是对下面这个精确字符串(以 \n 连接)计算的 HMAC:
TEA1-HMAC-SHA256
<METHOD>
<path>
<canonicalQuery>
<timestamp>
<nonce>
<bodyHash>
<METHOD>—— 大写(GET/POST)。<path>—— 完整路径,包含/api/v1前缀(例如/api/v1/energy/quotes)。<canonicalQuery>—— 按下述规则规范化的查询字符串;无查询字符串时为空。<timestamp>、<nonce>、<bodyHash>—— 对应请求头的取值。
查询字符串规范化
- 按
&分割,丢弃空片段。 - 解码每个键与值:
+→ 空格,再做百分号解码。 - 先按键排序,再按值排序。
- 用 RFC 3986 百分号编码重新编码每个键与值 —— 只有
A–Z a–z 0–9 - . _ ~保持字面(!会变成%21)。
例如 page=2¬e=hello!world 规范化为:
note=hello%21world&page=2
测试向量
下面的向量是冻结的已知答案 —— 你的客户端必须逐字节复现。它们使用 secret = sk_test_0123456789abcdef0123456789abcdef。
request-basic
| 字段 | 取值 |
|---|---|
| method | POST |
| path | /api/v1/energy/orders |
| rawQuery | (空) |
| body | {"amount":64400,"duration_minutes":60} |
| timestamp | 1727520000 |
| nonce | 0123456789abcdef0123456789abcdef |
| bodyHash | 376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04 |
| signature | c749398f7d8b55d6b7b9e2332d5b5ee20006e5e950ba873f0f9077357973ded3 |
规范字符串:
TEA1-HMAC-SHA256
POST
/api/v1/energy/orders
1727520000
0123456789abcdef0123456789abcdef
376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04
参考实现
每个实现都能复现 request-basic 向量。直接运行即可通过自检;对于真实请求,请用当前时钟生成 timestamp,用 CSPRNG 生成 nonce(os.urandom(16).hex() / crypto/rand / getrandom)。Python 与 Go 实现仅使用标准库;Rust 实现需要 hmac = "0.12"、sha2 = "0.10"、hex = "0.4"。
import hashlib, hmac, os, time from urllib.parse import quote, unquote_plus ALGORITHM = "TEA1-HMAC-SHA256" def rfc3986_encode(value): # 仅保留 RFC 3986 非保留字符:A-Z a-z 0-9 - . _ ~ return quote(value, safe="-._~") def canonicalize_query(raw_query): if not raw_query: return "" pairs = [] for part in raw_query.split("&"): if not part: continue k, _, v = part.partition("=") pairs.append((unquote_plus(k), unquote_plus(v))) pairs.sort(key=lambda kv: (kv[0], kv[1])) return "&".join(f"{rfc3986_encode(k)}={rfc3986_encode(v)}" for k, v in pairs) def sign(secret, method, path, raw_query="", body="", timestamp=None, nonce=None): timestamp = timestamp or str(int(time.time())) nonce = nonce or os.urandom(16).hex() body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest() canonical = "\n".join([ALGORITHM, method.upper(), path, canonicalize_query(raw_query), timestamp, nonce, body_hash]) signature = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest() return {"x-timestamp": timestamp, "x-nonce": nonce, "x-body-hash": body_hash, "x-signature": signature} # 对照已发布向量自检 headers = sign( "sk_test_0123456789abcdef0123456789abcdef", "POST", "/api/v1/energy/orders", body='{"amount":64400,"duration_minutes":60}', timestamp="1727520000", nonce="0123456789abcdef0123456789abcdef", ) assert headers["x-body-hash"] == "376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04" assert headers["x-signature"] == "c749398f7d8b55d6b7b9e2332d5b5ee20006e5e950ba873f0f9077357973ded3"
