Skip to content

HMAC 鑑權

Coinepay OpenAPI 使用 HMAC-SHA256 對每次請求籤名。客戶必須在每次請求頭中提供 4 個值;伺服器據此校驗身份 + 時間窗 + 防重放 + 防篡改

必需的 4 個請求頭

Header說明示例
X-App-Id憑證標識,固定 31 字元(cp_ + 28 hex)cp_a1b2c3d4e5f6071829304a5b6c7d8e9f
X-TimestampUnix (不是毫秒),ASCII 十進位制1714377600
X-Nonce8~64 位字元,10 分鐘內對同一 AppID 唯一8f7e6d5c4b3a29180a1b2c3d4e5f6071
X-SignatureHMAC-SHA256 lowercase hex,64 字元9b8e7f6d5c4b3a...

時間單位

X-Timestamp 必須是,不是毫秒。Math.floor(Date.now() / 1000) 不是 Date.now()

簽名輸入構造

text
signInput = METHOD + LF + PATH + LF + RAW_QUERY + LF + TIMESTAMP + LF + NONCE + LF + BODY_SHA256_HEX
符號含義
LF字元 \n(單位元組 0x0A),不是 \r\n
+字串拼接

各欄位定義

欄位取值注意
METHOD全大寫 HTTP 方法(OpenAPI 全部為 POSTASCII
PATH請求路徑含開頭 /不含查詢串不要 URL decode/encode
RAW_QUERY查詢串(不含 ?),無則空字串通常為空
TIMESTAMPX-Timestamp完全相同字串ASCII
NONCEX-Nonce 頭完全相同ASCII
BODY_SHA256_HEXsha256(請求體位元組) 的 lowercase hex64 字元

空 body 的 sha256

固定為 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。 空物件 {} 的 sha256 是 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a(注意區別)。

簽名計算

text
signature = lowercase_hex( HMAC_SHA256( secret_bytes, signInput_bytes ) )

Secret 編碼

secret_bytes 是 secret 字串的 UTF-8 位元組(直接是 64 個 hex 字元的 ASCII 位元組)。 不要把 hex decode 成 32 位元組再做 HMAC。

簽名長度固定為 64 字元 hex

完整請求示例

http
POST /api/v1/openapi/card_headers/list HTTP/1.1
Host: api.coinepay.net
X-App-Id: cp_a1b2c3d4e5f6071829304a5b6c7d8e9f
X-Timestamp: 1714377600
X-Nonce: 8f7e6d5c4b3a29180a1b2c3d4e5f6071
X-Signature: 9b8e7f6d5c4b3a2918273645d4e3c2b1a0f9e8d7c6b5a4938271605f4e3d2c1b
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
Content-Length: 24

{"page":1,"page_size":20}

防重放視窗

時間戳容差[server_now - 300s, server_now + 300s] (±5 分鐘)
Nonce 唯一視窗同一 (app_id, nonce)10 分鐘內不允許重複
推薦 Nonce 生成crypto/rand 16 位元組 → 32 hex 字元

極端情況速查

場景處理
body 是空物件 {}BODY_SHA256_HEX = 44136fa3...8a
body 是空字串用空 body sha256 常量 e3b0c44...855
body 含中文UTF-8 位元組計算 sha256
body 是陣列 [1,2,3]按位元組字面計算(key 排序、空格不影響雜湊)
網路中介軟體改 body簽名失敗;避免任何中間層改 body

實現注意事項

客戶端 stringify 必須確定性

不同語言的 JSON 序列化可能有鍵序、空格、轉義差異。客戶端 stringify 出來的位元組是什麼,就用什麼計算 sha256,傳給伺服器的也是同一份位元組。不要先 stringify 計算 hash、又用另一種序列化傳送。

401 錯誤處理

伺服器對所有鑑權失敗原因(AppID 不存在 / 簽名錯 / 時間漂移 / 憑證停用 / 使用者不活躍)都返回相同的 openapi_invalid_credentials。這是反列舉設計,客戶端無需根據具體原因區分。

排查步驟:

  1. 確認伺服器時間與客戶端時間差 < 5 分鐘(用 date +%s 對一下)
  2. 列印 signInput 的位元組,逐行確認 \n0x0A、沒有 \r
  3. 確認 X-Timestamp 頭與 signInput 中的 TIMESTAMP 完全一致(不是各算一次)
  4. 確認 BODY_SHA256_HEX 的 body 是實際傳送的位元組(不是 stringify 又改過的)
  5. 重置一對憑證再試

下一步

採用 MIT 等價條款釋出