> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kyrenpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook 簽名驗證

> 驗證 Webhook 請求的真實性

啟潤支付的每個 Webhook 請求都包含一個加密簽名，你應該驗證此簽名以確保請求是真實的且未被篡改。

## 簽名演算法

啟潤支付使用 **HMAC-SHA256** 對 Webhook 載荷進行簽名：

```
signature = "sha256=" + HMAC-SHA256(timestamp + "." + payload, webhook_secret)
```

簽名和時間戳透過 HTTP 請求標頭發送：

| 請求標頭                | 範例                          |
| ------------------- | --------------------------- |
| `X-Kyren-Signature` | `sha256=5d2dc4f1a...`       |
| `X-Kyren-Timestamp` | `1704628800000`（Unix 毫秒時間戳） |

## 驗證步驟

<Steps>
  <Step title="提取請求標頭">
    從請求標頭中讀取 `X-Kyren-Signature` 和 `X-Kyren-Timestamp`。
  </Step>

  <Step title="檢查時間戳">
    驗證時間戳在當前時間的 5 分鐘（300000 毫秒）以內。這可以防止重放攻擊。
  </Step>

  <Step title="計算預期簽名">
    拼接 `timestamp + "." + 原始請求本體`，然後使用你的 Webhook 密鑰計算 HMAC-SHA256。
  </Step>

  <Step title="比較簽名">
    將計算出的簽名與請求標頭中的簽名進行比較。使用常數時間比較以防止時序攻擊。
  </Step>
</Steps>

## 程式碼範例

### Node.js

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, timestamp, secret) {
  // 檢查時間戳容差（5 分鐘）
  const currentTime = Date.now();
  if (Math.abs(currentTime - parseInt(timestamp)) > 300000) {
    return false;
  }

  // 計算預期簽名
  const data = `${timestamp}.${payload}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(data)
    .digest('hex');

  // 常數時間比較
  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  } catch {
    return false;
  }
}

// 在 Express 中使用
app.post('/webhooks/kyren',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const isValid = verifyWebhookSignature(
      req.body.toString(),
      req.headers['x-kyren-signature'],
      req.headers['x-kyren-timestamp'],
      process.env.KYREN_WEBHOOK_SECRET
    );

    if (!isValid) {
      return res.status(400).send('Invalid signature');
    }

    // 處理事件...
    res.status(200).send('OK');
  }
);
```

### Python

```python theme={null}
import hmac
import hashlib
import time

def verify_webhook_signature(payload: str, signature: str, timestamp: str, secret: str) -> bool:
    # 檢查時間戳容差（5 分鐘）
    current_time = int(time.time() * 1000)
    if abs(current_time - int(timestamp)) > 300000:
        return False

    # 計算預期簽名
    data = f"{timestamp}.{payload}"
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        data.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    # 常數時間比較
    return hmac.compare_digest(expected, signature)


# 在 Flask 中使用
from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/kyren", methods=["POST"])
def webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get("X-Kyren-Signature", "")
    timestamp = request.headers.get("X-Kyren-Timestamp", "")

    if not verify_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET):
        return "Invalid signature", 400

    event = request.get_json()
    # 處理事件...

    return "OK", 200
```

### Go

```go theme={null}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"math"
	"net/http"
	"strconv"
	"time"
)

func verifyWebhookSignature(payload, signature, timestamp, secret string) bool {
	// 檢查時間戳容差（5 分鐘）
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return false
	}
	currentTime := time.Now().UnixMilli()
	if math.Abs(float64(currentTime-ts)) > 300000 {
		return false
	}

	// 計算預期簽名
	data := fmt.Sprintf("%s.%s", timestamp, payload)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(data))
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

	// 常數時間比較
	return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	payload := string(body)
	signature := r.Header.Get("X-Kyren-Signature")
	timestamp := r.Header.Get("X-Kyren-Timestamp")

	if !verifyWebhookSignature(payload, signature, timestamp, webhookSecret) {
		http.Error(w, "Invalid signature", http.StatusBadRequest)
		return
	}

	// 處理事件...
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}
```

<Warning>
  **重要安全注意事項：**

  * 處理事件前必須先驗證簽名
  * 使用常數時間比較函數以防止時序攻擊
  * 拒絕時間戳超過 5 分鐘（300000 毫秒）的請求以防止重放攻擊
  * 使用原始請求本體進行簽名驗證（JSON 解析之前）
</Warning>
