> ## 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 會使用以你的**簽署密鑰**為金鑰的 [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 動作簽章。並沒有針對個別動作的簽署密鑰——設定或輪替密鑰時，該 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 的密鑰參照重新指向新密鑰，並且——當先前已有密鑰存在時——將被取代的密鑰記錄為前一版密鑰，並設定 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 的雜湊值），永遠不會是 UID 本身。`Profile-Key` 標頭為選用，對多 profile 帳號可將此輪替範圍限定於單一 User Profile。

若缺少或提供空的 `secret`，會回傳對應的錯誤（`code: 101`，「Missing/incorrect parameter」）及 HTTP `400` 狀態，且不會變更你目前的密鑰。透過 API 首次設定密鑰（尚無既有密鑰）時，會建立密鑰但不記錄前一版密鑰，也不會有寬限期。

## 安全輪替流程

由於有 24 小時的寬限期，並沒有一定要遵循的操作順序——過程中你的接收端會持續正常運作。建議的順序如下：

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

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

  <Step title="讓寬限期結束">
    24 小時之後，Ayrshare 會自動清除前一版密鑰，並僅以新密鑰簽章。你這邊不需要再做任何動作。
  </Step>
</Steps>

<Tip>
  如果在寬限期尚未結束時再次輪替，剛才被取代的密鑰會成為新的前一版密鑰，並重新開始一段 24 小時的寬限期。任何時候只會保留一份前一版密鑰。
</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` 標頭會列出**兩組**簽章，目前的簽章在前，以逗號分隔：

```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` 維持不變：為了向下相容，它一律只帶目前密鑰的單一 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">
    使用你本機設定的簽署密鑰，計算**原始 request body** 的 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>
  請一律針對**原始的** request body 位元組（收到什麼就用什麼）計算 HMAC——不要對重新序列化過的 JSON 物件計算。重新序列化可能會改變空白或鍵值順序，導致驗證失敗。請使用常數時間比較（例如 `crypto.timingSafeEqual`）以避免時序攻擊。
</Warning>

若某次傳遞的目前密鑰紀錄遺失，該次傳遞會以**未簽章**（沒有簽章標頭）的形式送出，而不是失敗。若只有前一版密鑰的紀錄遺失，則會略過前一版簽章，仍然會在兩個標頭中送出目前的簽章。
