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

# 輪替簽章密鑰

> 以 24 小時雙簽章寬限視窗安全輪替你的 Webhook 簽章密鑰

export const PlansAvailable = ({plans = [], maxPackRequired}) => {
  let displayPlans = plans;
  if (plans && plans.length === 1) {
    const lowerCasePlan = plans[0].toLowerCase();
    if (lowerCasePlan === "business") {
      displayPlans = ["Launch", "Business", "Enterprise"];
    } else if (lowerCasePlan === "premium") {
      displayPlans = ["Premium", "Launch", "Business", "Enterprise"];
    }
  }
  return <Note>
Available on {displayPlans.length === 1 ? "the " : ""}
{displayPlans.join(", ").replace(/\b\w/g, l => l.toUpperCase())}{" "}
{displayPlans.length > 1 ? "plans" : "plan"}.

{maxPackRequired && <span onClick={() => window.open('https://www.ayrshare.com/docs/additional/maxpack', '_self')} className="flex items-center mt-2 cursor-pointer">
 <span className="px-1.5 py-0.5 rounded text-sm" style={{
    backgroundColor: '#C264B6',
    color: 'white',
    fontSize: '12px'
  }}>
   Max Pack required
 </span>
</span>}
</Note>;
};

export const HeaderAPI = ({noProfileKey, profileKeyRequired}) => <>
    <ParamField header="Authorization" type="string" required>
      <a href="/docs/apis/overview#authorization">API Key</a> of the Primary Profile.
      <br />
      <br />
      Format: <code>Authorization: Bearer API_KEY</code>
    </ParamField>
    {!noProfileKey && (profileKeyRequired ? <ParamField header="Profile-Key" type="string" required>
          <a href="/docs/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
          <br />
          <br />
          Format: <code>Profile-Key: PROFILE_KEY</code>
        </ParamField> : <ParamField header="Profile-Key" type="string">
          <a href="/docs/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
          <br />
          <br />
          Format: <code>Profile-Key: PROFILE_KEY</code>
        </ParamField>)}
  </>;

<PlansAvailable plans={["premium"]} maxPackRequired={false} />

## 概觀

Ayrshare 會以你的**簽章密鑰**為金鑰，使用 payload 的 [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC) 對每一次 Webhook 傳遞進行簽章，讓你的接收端能確認該傳遞確實來自 Ayrshare。驗證方式請參閱 [Webhook 安全性](/docs/apis/webhooks/overview#webhook-security)。

輪替簽章密鑰讓你可以按固定排程更換密鑰，或在懷疑密鑰外洩時立即更換。為了讓輪替安全進行，Ayrshare 會在每次輪替後開啟一個 **24 小時寬限視窗**，這段期間內的傳遞會同時使用你的**舊密鑰與新密鑰**簽章。這讓你可以依自己的節奏更新接收端，且不會丟失或拒絕任何一次傳遞——這與 Stripe 和 GitHub 使用的模式相同。

<Note>
  簽章密鑰為 **profile 層級**：每個 User Profile（UID）只有一個密鑰，
  該密鑰會為該 profile 註冊的**每一個** Webhook 動作簽章。並沒有
  per-action 的簽章密鑰——設定或輪替密鑰時，會同時變更該 profile 上
  所有動作的密鑰。
</Note>

## 從控制台輪替

你可以在開發者控制台的 [Webhooks 頁面](https://app.ayrshare.com/webhooks) 設定或輪替簽章密鑰。當該 profile 至少註冊一個 Webhook 時，**Signing Secret** 面板會出現在 Webhook 清單上方。

<Steps>
  <Step title="開啟 Signing Secret 面板">
    前往 [Webhooks 頁面](https://app.ayrshare.com/webhooks)。若尚未設定密鑰，面板會顯示 **No signing secret configured** 及 **Set Signing Secret** 按鈕。若已設定密鑰，則會顯示 **Signing secret configured** 及 **Rotate** 按鈕。
  </Step>

  <Step title="設定或輪替">
    點擊 **Set Signing Secret**（首次設定）或 **Rotate**（既有密鑰）。會開啟一個 modal，其中已預先填入並顯示一組強隨機產生的密鑰。你可以 **Copy** 密鑰、**Regenerate** 產生新的一組，或切換 **paste my own** 以提供自己的值。
  </Step>

  <Step title="確認">
    將密鑰複製並妥善保存——它只會顯示一次，之後無法再從 UI 取得——接著確認送出。會出現成功提示，面板也會更新。
  </Step>

  <Step title="更新你的接收端">
    在輪替（而非首次設定）時，面板會顯示啟用中的寬限視窗指示器，且 **Rotate** 按鈕在視窗關閉前會被停用。你有 24 小時的時間將新密鑰部署到接收端。
  </Step>
</Steps>

## 透過 API 輪替

只需一次呼叫即可輪替（或設定）簽章密鑰。這會建立一組新密鑰、將該 profile 的密鑰參照指向該新密鑰，並在原本已存在密鑰的情況下，將被取代的密鑰記錄為 previous secret，並設定 24 小時後到期。

### 標頭參數

<HeaderAPI />

### Body 參數

<ParamField body="secret" type="string" required>
  新的簽章密鑰值。任何非空字串皆可接受。建議使用高熵、較長的隨機值（例如以 base64url 編碼的 32 個隨機位元組）。
</ParamField>

<RequestExample>
  ```bash cURL theme={"system"}
  curl --request POST \
    --url https://api.ayrshare.com/api/hook/webhook/secret \
    --header 'Authorization: Bearer YOUR_API_KEY' \
    --header 'Content-Type: application/json' \
    --header 'Profile-Key: YOUR_PROFILE_KEY' \
    --data '{
      "secret": "your-new-signing-secret"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200: Response theme={"system"}
  {
    "status": "success",
    "action": "webhook",
    "refId": "3dc079614bdc3f281d9" // User Profile Ref Id
  }
  ```
</ResponseExample>

明文的 `secret` **絕不會**於回應中回傳，也不會被記錄。回應會攜帶對外的 `refId`（UID 的 hash），絕不會攜帶 UID 本身。`Profile-Key` 標頭為選用，用於在多 profile 帳號中將輪替限縮至單一 User Profile。

若 `secret` 缺少或為空，會回傳對應的錯誤（`code: 101`，"Missing/incorrect parameter"）並帶有 HTTP `400` 狀態，且不會變更你目前的密鑰。透過 API 首次設定（無既有密鑰）會建立密鑰，且不會記錄 previous secret 也不會啟動寬限視窗。

## 安全輪替流程

由於有 24 小時寬限視窗，並沒有必要的操作先後順序——你的接收端在整個過程中都能正常運作。建議的順序如下：

<Steps>
  <Step title="輪替密鑰">
    從控制台或透過 API 輪替。Ayrshare 會立即開始同時使用舊密鑰與新密鑰為傳遞簽章。
  </Step>

  <Step title="更新你的接收端">
    在 24 小時內，將新密鑰部署到你的 Webhook 接收端，讓它以新密鑰進行驗證。
  </Step>

  <Step title="讓視窗自然關閉">
    24 小時後，Ayrshare 會自動清除舊密鑰並只以新密鑰簽章。你這一端不需要再做任何動作。
  </Step>
</Steps>

<Tip>
  若你在寬限視窗仍開啟時再次輪替，剛剛被取代的密鑰會成為新的
  previous secret，並開啟一個新的 24 小時視窗。每次只會保留
  一個 previous secret。
</Tip>

## 在寬限視窗期間驗證簽章

在寬限視窗以外，已簽章的傳遞會攜帶標準標頭（請參閱 [Webhook 安全性](/docs/apis/webhooks/overview#webhook-security)）：

```bash theme={"system"}
X-Authorization-Timestamp : <Unix Timestamp In Seconds>
X-Authorization-Content-SHA256 : <current-sig>
X-Authorization-Content-SHA256-V2 : v1=<current-sig>
```

在輪替後的 24 小時視窗內，新的 `X-Authorization-Content-SHA256-V2` 標頭會列出**兩組**簽章，以 current 在前、逗號分隔的形式呈現：

```bash theme={"system"}
X-Authorization-Timestamp : <Unix Timestamp In Seconds>
X-Authorization-Content-SHA256 : <current-sig>
X-Authorization-Content-SHA256-V2 : v1=<current-sig>,v1=<previous-sig>
```

<Note>
  `X-Authorization-Content-SHA256` 沒有變化：它一律只攜帶單一
  以 current secret 計算的 HMAC，以維持向下相容。雙簽章只會出現在
  新的 `X-Authorization-Content-SHA256-V2` 標頭中。
</Note>

`X-Authorization-Content-SHA256-V2` 中的每個值都會以 scheme 標籤作為前綴。`v1=` 表示 HMAC-SHA256 簽章，計算方式與 `X-Authorization-Content-SHA256` 完全相同。只要傳遞有簽章，`-V2` 標頭就一定會出現——至少會攜帶 `v1=<current-sig>`——因此你可以將它視為穩定的接收端契約來依賴。

要在輪替期間（或平時）驗證一次傳遞：

<Steps>
  <Step title="計算 HMAC">
    使用你接收端本地設定的簽章密鑰，對**原始請求主體**計算 HMAC-SHA256。
  </Step>

  <Step title="與每個列出的簽章比對">
    讀取 `X-Authorization-Content-SHA256-V2`，以逗號分割，將每個值去除 `v1=` 前綴，若你計算出的 HMAC 與**任一**列出的 `v1=` 簽章相符，就視為傳遞為真。
  </Step>
</Steps>

「只要**任一**列出的簽章相符就接受」的作法是輪替能達成零停機的關鍵：仍設定舊密鑰的接收端會匹配到 `v1=<previous-sig>`，已更新為新密鑰的接收端則會匹配到 `v1=<current-sig>`——在整個視窗期間兩者都能通過。

### 接收端驗證範例

```javascript Node.js theme={"system"}
import crypto from "crypto";

// secret is the signing secret currently configured on your receiver.
function isAuthenticWebhook(rawBody, headers, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody) // the raw, unparsed request body
    .digest("hex");

  const headerValue = headers["x-authorization-content-sha256-v2"] || "";

  // Accept if ANY v1= signature in the header matches our computed HMAC.
  return headerValue
    .split(",")
    .map((part) => part.trim())
    .filter((part) => part.startsWith("v1="))
    .map((part) => part.slice("v1=".length))
    .some((sig) => {
      const sigBuf = Buffer.from(sig);
      const expectedBuf = Buffer.from(expected);
      // timingSafeEqual throws on length mismatch — treat as not authentic.
      return (
        sigBuf.length === expectedBuf.length &&
        crypto.timingSafeEqual(sigBuf, expectedBuf)
      );
    });
}
```

<Warning>
  請務必針對**原始**請求主體位元組（即收到的內容）計算 HMAC，
  而非重新序列化過的 JSON 物件。重新序列化可能會改變空白或鍵順序，
  導致驗證失敗。請使用常數時間比較（例如 `crypto.timingSafeEqual`）
  以避免時序攻擊。
</Warning>

若某次傳遞的 current secret 記錄遺失，該傳遞會以**未簽章**（無簽章標頭）的方式進行，而不會失敗。若僅 previous secret 記錄遺失，previous signature 會被略過，但 current signature 仍會在兩個標頭中發出。
