<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Webhook on Tarragon</title><link>https://tarrragon.github.io/blog/tags/webhook/</link><description>Recent content in Webhook on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 29 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/webhook/index.xml" rel="self" type="application/rss+xml"/><item><title>7.35 簽章對接的驗證收斂：驗簽通過之後還缺哪一塊</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/signature-integration-verification/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/signature-integration-verification/</guid><description>&lt;p>驗簽通過之後還有三件事要做，而它們在功能測試裡都不會出現。&lt;/p>
&lt;p>這一章要交出去的是三樣東西：一份雙方確認過的驗證素材規格、一個換算得出上下界的窗口長度、以及一組分得開的拒絕原因。第一樣決定對接要花多久，第二樣決定驗簽通過之後還擋不擋得住重送，第三樣決定事件當天查得出方向。另有一項不必等對方就能修完：比對驗證值那一行用對函式。&lt;/p>
&lt;h2 id="本章涵蓋與不涵蓋">本章涵蓋與不涵蓋&lt;/h2>
&lt;p>本章的三個收斂條件涵蓋範圍不同：驗證素材的對齊是共享密鑰簽章專屬，而重放窗口對任何接收外部推送的端點都成立（與用哪一種機制無關），比對方式對任何做等值比對的秘密都成立（API key 的比對有一模一樣的時間洩漏）。機制本身還沒選的讀者從 &lt;a href="../machine-credential-mechanism-selection/">7.34 機器憑證的機制選型&lt;/a> 進來；這個機制屬於哪一類原語、密鑰放在哪一格見 &lt;a href="../cryptographic-primitive-selection/">7.28 密碼學原語選型&lt;/a>；密鑰的保存與輪替屬治理層，見 &lt;a href="../secrets-and-machine-credential-governance/">7.6 秘密管理與機器憑證治理&lt;/a>。本章聚焦選定之後的收斂條件，機制本身的原理在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/message-authentication/" data-link-title="Message Authentication" data-link-desc="兩個系統用共享密鑰互相呼叫時，用來判斷驗證值保護到什麼範圍、撤銷粒度落在哪一層">Message Authentication&lt;/a>。下方反覆出現的&lt;strong>驗證素材&lt;/strong>指的是進入計算的那串內容——哪些欄位、以什麼順序、用什麼編碼串接起來。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：驗證素材的定義兩端不一致 / 重放窗口未收斂 / 驗證值的比對方式抵銷機制強度。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>該不該用共享密鑰簽章、與其他機制的取捨 → &lt;a href="../machine-credential-mechanism-selection/">7.34&lt;/a>&lt;/li>
&lt;li>密鑰放在哪一格、被誰拿得到 → &lt;a href="../cryptographic-primitive-selection/">7.28&lt;/a>&lt;/li>
&lt;li>密鑰的保存與輪替節奏 → &lt;a href="../secrets-and-machine-credential-governance/">7.6&lt;/a>&lt;/li>
&lt;li>素材規格變更時的相容判斷（本章不談版本策略，只到「規格要雙方共同確認」為止）→ &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/backward-compatibility-discipline/" data-link-title="11.6 向後相容的變更紀律" data-link-desc="哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律">11.6 向後相容的變更紀律&lt;/a> 的抽象層紀律&lt;/li>
&lt;/ul>
&lt;p>out-of-scope 的議題直接跳到對應章節。&lt;/p>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;p>本章是 routing layer，沿兩條 chain 進入 implementation：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表「前置控制面」欄的連結進知識卡，看該控制的機制、邊界與適用條件。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：「交接路由」欄位指向 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理&lt;/a>。重放窗口那一列走 04，因為拒絕原因分不分得開是監控設計的問題。&lt;/li>
&lt;/ul>
&lt;p>兩條 chain 完成判準與模組級 chain 規格見 &lt;a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain&lt;/a>。&lt;/p>
&lt;h2 id="問題節點案例觸發式">問題節點（案例觸發式）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題節點&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>風險後果&lt;/th>
 &lt;th>前置控制面&lt;/th>
 &lt;th>交接路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>驗證素材定義不一致&lt;/td>
 &lt;td>兩端各自解讀欄位順序、單位與空值處理&lt;/td>
 &lt;td>對接失敗且錯誤訊息無法定位&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/message-authentication/" data-link-title="Message Authentication" data-link-desc="兩個系統用共享密鑰互相呼叫時，用來判斷驗證值保護到什麼範圍、撤銷粒度落在哪一層">message-authentication&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">api-contract&lt;/a>&lt;/td>
 &lt;td>&lt;code>05&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重放窗口未收斂&lt;/td>
 &lt;td>驗證值涵蓋時間戳但接收端未檢查新鮮度&lt;/td>
 &lt;td>攔截到的請求可無限重放&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-attack/" data-link-title="Replay Attack" data-link-desc="攔截到的合法請求被原封不動再送一次時，用來判斷哪一層該負責擋、以及擋不住會發生什麼">replay-attack&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">idempotency-key&lt;/a>&lt;/td>
 &lt;td>&lt;code>04 + 06 + 08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>比對方式抵銷強度&lt;/td>
 &lt;td>驗證值用一般字串相等運算比對&lt;/td>
 &lt;td>回應時間洩漏吻合前綴的長度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timing-attack/" data-link-title="Timing Attack" data-link-desc="比對密鑰、token 或簽章的程式碼要判斷是否會由執行時間洩漏資訊時的依據">timing-attack&lt;/a>&lt;/td>
 &lt;td>&lt;code>05&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀流程">判讀流程&lt;/h2>
&lt;ol>
&lt;li>先判這個端點需不需要時間軸的保護：重送一次會不會產生第二次副作用。不會的純查詢端點到這裡就結束，剩下兩項仍然要做。&lt;/li>
&lt;li>需要的話定窗口長度。第一題是對方的時間戳在事件產生時簽還是每次送出時簽，它決定下界怎麼算；換算與上下界交叉時的處置見下方「重放窗口的收斂條件」。&lt;/li>
&lt;li>接著確認素材規格是雙方共同確認過的一份，而不是各自一份文件。缺的話在第一次對接時補。自助式的供應商沒有那個對話（註冊完就上線、沒有窗口可談），這時退到單方規格：把實際收到的請求原樣存下幾筆當成規格的依據、據此寫回歸測試，並在文件裡標明依據是實測而非對方的文件。這樣做的差別在對方改了東西時測試會紅，而不是等到驗簽開始失敗才發現。&lt;/li>
&lt;li>再查程式碼裡比對驗證值那一行用的是不是等時比較函式。&lt;/li>
&lt;li>最後把窗口長度、素材規格、拒絕原因的分類與第 4 步的檢查結果寫下來。第 1 步判定為不需要時同樣要寫，記的是判定依據（這個端點重送不產生第二次副作用）——否則判定過而判定不需要，與根本沒判過，在產物上看起來一樣。自己是推送方時這幾項屬於對外契約的內容；自己是接收方時對外契約在對方手上，這幾項落在自己的整合紀錄與監控設定裡。兩種情形共同的最低要求是拒絕原因分得開，事件當下才查得出方向。&lt;/li>
&lt;/ol>
&lt;h2 id="驗證素材的對齊成本">驗證素材的對齊成本&lt;/h2>
&lt;p>素材定義不一致是簽章對接裡反覆發生的耗時來源，特徵是&lt;strong>失敗訊號不具指向性&lt;/strong>：驗證值對不起來時只會得到一個布林值，看不出是欄位少了一個、順序反了、還是時間戳的單位是秒而對方送的是毫秒。&lt;/p>
&lt;p>這個特徵決定了它的成本結構：耗時不隨團隊能力下降，而是隨介面調整次數重複發生。每一次新增欄位、改變編碼、調整空值處理，兩端都要重新對齊一次，而每一次對齊都要付同樣的排錯成本。因此把素材規格寫進雙方共同的書面契約，這筆投資在整合的生命週期裡會被攤提多次——而寫進去的時機在第一次對接時成本最低，那時兩邊的人都還在同一個對話裡。&lt;/p>
&lt;p>規格要涵蓋的項目、可在本機單方完成的診斷手法（先讓兩端各自對同一份輸入算一次，比對中間值而非最終值）與對接的檢查順序見 &lt;a href="https://tarrragon.github.io/blog/work-log/hmac_signature_field_alignment/" data-link-title="HMAC 簽章對接：對不上的是輸入定義、用確定性反推它落在哪一端" data-link-desc="兩端 HMAC 算不出同一個值時要逐項核對的輸入定義，以及用簽章反推輸入、判斷問題落在哪一端的除錯手法。">HMAC 簽章對接&lt;/a>。&lt;/p>
&lt;p>&lt;strong>驗證素材定義不一致&lt;/strong>要有兩個團隊各自實作才會發生，因此它的密度與對接對象數量成正比。跨組織整合、集團內跨產品線、以及自己的服務對接第三方的 webhook 都落在這裡。識別特徵是雙方各有一份描述簽章怎麼算的文件，而沒有一份是雙方共同確認過的。&lt;/p>
&lt;h2 id="重放窗口的收斂條件">重放窗口的收斂條件&lt;/h2>
&lt;p>哪些端點需要這一步，判準是重送一次會不會產生第二次副作用：轉帳、下單、發送通知、狀態轉移都會，純查詢端點不會。接收外部 webhook 的端點風險最高——請求來自自己控制範圍之外，攻擊者可能就在傳輸路徑上，而 webhook 的重試機制本身就會產生大量重複請求，讓惡意重放藏在正常重試裡。&lt;/p>
&lt;p>時間軸條件（驗證值涵蓋時間戳、接收端檢查新鮮度）擋住窗口外的重送，窗口內的重複由識別值去重承接，兩者的分工見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/replay-attack/" data-link-title="Replay Attack" data-link-desc="攔截到的合法請求被原封不動再送一次時，用來判斷哪一層該負責擋、以及擋不住會發生什麼">Replay Attack&lt;/a>。這個節點的特殊之處在於缺的那一步從來沒有被排進工作項：驗簽做完了、測試綠了、上線了，而「還要比對時間戳與識別值」這件事沒有出現在任何一張清單上，所以它不是做失敗、是沒有人想到要做。&lt;/p>
&lt;p>它的失敗長這樣：webhook 接收端照對方文件實作了簽章驗證，驗簽通過就代表這個請求出自對方，當下沒有人覺得還缺什麼——時間戳與識別值都在 payload 裡，兩者都沒有人比對。某次對方那端重試堆積之後，同一筆扣款被處理了好幾次。查起來每一筆都通過驗簽、每一筆的內容都合法，監控上是一串正常請求；而 webhook 本來就會重試，重複請求與惡意重放在日誌裡沒有任何欄位分得開。&lt;/p>
&lt;p>補救的順序由重複落在哪一側決定，而這個案例落在窗口內側：重試堆積發生在幾秒到幾分鐘之內，新鮮度檢查放行它是正確行為，擋得住它的是識別值去重。所以先建去重儲存，再補時間戳的新鮮度檢查把窗口外的重送一併擋掉——兩道控制的分工見上一段。已經發生的重複副作用要逐筆對帳回滾，那部分的工作量由這個缺口存在了多久決定。&lt;/p>
&lt;p>窗口長度是對接階段就要定的參數，兩端都能換算成具體的量。下界由對方的時間戳在什麼時候簽決定：送出時簽的話每次重試各自帶新的時間戳，下界就是量測到的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/clock-skew/" data-link-title="Clock Skew" data-link-desc="跨機器比較時間才成立的機制（時效窗口、憑證有效期、事件排序）在決定容忍值時的判斷依據">時鐘偏移&lt;/a> 上界加餘裕，落在秒到分鐘級；產生時簽的話整串重試共用同一個時間戳，下界要改取時鐘偏移上界與對方最大重試退避間隔的較大者，而後者通常大上幾個數量級——退避超過窗口的那些合法重試會被判成過期，日誌上與攻擊分不開。這一題要向對方確認，退避表查不到時當作產生時簽並取對方公告的重試總時長，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">指數退避&lt;/a> 是它最常見的形狀。&lt;/p>
&lt;p>上界由去重的儲存量反推：窗口內的已處理識別值都要留著，請求速率乘上窗口長度就是要保留的筆數。這個數字撐不住時要動的是儲存形態而非窗口——存識別值的雜湊而非原值、讓儲存自己按存活時間淘汰——因為把窗口縮到下界以下會開始拒絕合法的重試，那與攻擊在日誌上分不開。兩端交叉到怎麼調都撐不住時，剩下的路是接受窗口外的重複由業務層的冪等承接，收斂點見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">Idempotency Key&lt;/a>。涉及金流或不可逆動作的端點直接取下界，不必在區間裡挑。&lt;/p>
&lt;p>&lt;strong>重放窗口未收斂&lt;/strong>出現在接收外部推送的端點，尤其是那些照著對方文件實作驗簽就上線的整合。這一格的成因與其餘節點不同：它不是做錯，是做完之後少做一步，因此它的出現率與驗簽實作的正確性無關——寫得越乾淨的實作越容易停在這裡，因為驗簽那一段看起來已經完成了。識別特徵是 payload 裡帶著時間戳與識別值，而接收端的程式碼裡找不到比對它們的地方。&lt;/p>
&lt;h2 id="比對方式抵銷機制強度">比對方式抵銷機制強度&lt;/h2>
&lt;p>重算出的值要用等時比較來比對。一般的字串相等運算在第一個不吻合的位元組就回傳，回應時間因此洩漏吻合前綴的長度，攻擊者可以逐位元組推出正確的驗證值，收斂點見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timing-attack/" data-link-title="Timing Attack" data-link-desc="比對密鑰、token 或簽章的程式碼要判斷是否會由執行時間洩漏資訊時的依據">Timing Attack&lt;/a>。&lt;/p>
&lt;p>&lt;strong>比對方式抵銷強度&lt;/strong>出現在自己動手實作驗簽的服務。用對方提供的 SDK 多半不會踩到，因為那一行藏在函式庫裡（多半而非一定——SDK 沒覆蓋到的框架仍要自己寫）；照文件自己寫的會踩到，因為文件多半只說明怎麼算出那個值、不說怎麼比對它。識別動作是查程式碼裡比對驗證值那一行。&lt;/p>
&lt;p>各語言都有現成的等時比較函式（&lt;code>hash_equals&lt;/code>、&lt;code>compare_digest&lt;/code>、&lt;code>ConstantTimeCompare&lt;/code> 這一類），判別方式是查程式碼裡比對驗證值那一行用的是不是那個函式。函式取用不到的環境還有第二條路：把收到的值與自己算出的值各再做一次 HMAC（用一把當場產生的隨機密鑰）之後比對，攻擊者無法預測比對的是什麼，時間差因此不再洩漏前綴長度。它與其餘節點的差別在它完全在自己這一側，不必與對方協調。&lt;/p>
&lt;h2 id="常見風險邊界">常見風險邊界&lt;/h2>
&lt;ul>
&lt;li>驗證素材沒有雙方共同的書面規格時，代表對接成本會在每次介面調整時重新發生，而那筆成本每次都由兩邊同時付。&lt;/li>
&lt;li>端點會產生第二次副作用而時間戳與識別值都沒有被比對時，保護只剩「這個請求出自對方」，攔截到的請求可以無限重放。&lt;/li>
&lt;li>窗口長度取得比量測到的時鐘偏移還短時，正常請求會在漂移時被拒，而那個拒絕在日誌裡與攻擊的表徵相同。&lt;/li>
&lt;li>監控把「驗證失敗」併成一個計數時，事件當下無法從監控判斷該往哪個方向查。三者指向不同：驗證值不符指向規格或密鑰不一致、時間戳過期指向時鐘偏移、識別值重複則是三者裡唯一需要區分惡意與正常重試的一種——它的絕大多數來源是對方的重試，所以不能只看計數，要查得到來源位址與時間分布。&lt;/li>
&lt;li>對方的文件與實際送出的內容不一致而對方不願修文件時，處置要往契約層走：把實測結果寫成雙方確認過的附件，並把自己這一側的實作依據記成「依實測而非依文件」。對方連附件都不給時這條整合帶著一個沒有書面依據的假設上線，期限與重評估條件走 &lt;a href="../security-governance-exception-and-tripwire/">7.14 資安治理例外與 Tripwire&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h2 id="案例觸發參考">案例觸發參考&lt;/h2>
&lt;ul>
&lt;li>簽章方案在對外契約上的實際形態： &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/webhook-stripe-delivery-contract/" data-link-title="11.C60 Stripe webhooks：at-least-once 加簽章、明文要求 consumer 冪等與不依賴順序" data-link-desc="webhook 對外承諾的教科書樣本：三天重試、重複投遞、no-ordering、簽章驗證的責任在同一頁明文轉移給 consumer">Stripe webhook 投遞契約&lt;/a>&lt;/li>
&lt;li>硬編碼憑證與固定認證路徑： &lt;a href="../red-team/cases/edge-exposure/usaherds-cve-2021-44207-hardcoded-credential/">USAHERDS 2021&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>上游（該不該用共享密鑰簽章）：&lt;a href="../machine-credential-mechanism-selection/">7.34 機器憑證的機制選型&lt;/a>&lt;/li>
&lt;li>機制原理與能力上限：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/message-authentication/" data-link-title="Message Authentication" data-link-desc="兩個系統用共享密鑰互相呼叫時，用來判斷驗證值保護到什麼範圍、撤銷粒度落在哪一層">Message Authentication&lt;/a>&lt;/li>
&lt;li>素材的逐項清單與診斷手法：&lt;a href="https://tarrragon.github.io/blog/work-log/hmac_signature_field_alignment/" data-link-title="HMAC 簽章對接：對不上的是輸入定義、用確定性反推它落在哪一端" data-link-desc="兩端 HMAC 算不出同一個值時要逐項核對的輸入定義，以及用簽章反推輸入、判斷問題落在哪一端的除錯手法。">HMAC 簽章對接&lt;/a>&lt;/li>
&lt;li>密鑰放在哪一格、被誰拿得到：&lt;a href="../cryptographic-primitive-selection/">7.28 密碼學原語選型&lt;/a>&lt;/li>
&lt;li>密鑰怎麼交到對方手上：&lt;a href="../machine-credential-issuance/">7.32 機器憑證的配發&lt;/a>&lt;/li>
&lt;li>密鑰的輪替與回收節奏：&lt;a href="../secrets-and-machine-credential-governance/">7.6 秘密管理與機器憑證治理&lt;/a>&lt;/li>
&lt;li>去重儲存與識別值的設計：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">Idempotency Key&lt;/a>&lt;/li>
&lt;li>素材規格新增欄位時兩端怎麼同步：本站尚無專章；通用的相容紀律見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/backward-compatibility-discipline/" data-link-title="11.6 向後相容的變更紀律" data-link-desc="哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律">11.6 向後相容的變更紀律&lt;/a>，在那之前的最小做法是把新欄位設成可選、兩端各自先支援「有或沒有都算對」一段時間，再切成必填&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>驗簽通過之後還有三件事要做，而它們在功能測試裡都不會出現。</p>
<p>這一章要交出去的是三樣東西：一份雙方確認過的驗證素材規格、一個換算得出上下界的窗口長度、以及一組分得開的拒絕原因。第一樣決定對接要花多久，第二樣決定驗簽通過之後還擋不擋得住重送，第三樣決定事件當天查得出方向。另有一項不必等對方就能修完：比對驗證值那一行用對函式。</p>
<h2 id="本章涵蓋與不涵蓋">本章涵蓋與不涵蓋</h2>
<p>本章的三個收斂條件涵蓋範圍不同：驗證素材的對齊是共享密鑰簽章專屬，而重放窗口對任何接收外部推送的端點都成立（與用哪一種機制無關），比對方式對任何做等值比對的秘密都成立（API key 的比對有一模一樣的時間洩漏）。機制本身還沒選的讀者從 <a href="../machine-credential-mechanism-selection/">7.34 機器憑證的機制選型</a> 進來；這個機制屬於哪一類原語、密鑰放在哪一格見 <a href="../cryptographic-primitive-selection/">7.28 密碼學原語選型</a>；密鑰的保存與輪替屬治理層，見 <a href="../secrets-and-machine-credential-governance/">7.6 秘密管理與機器憑證治理</a>。本章聚焦選定之後的收斂條件，機制本身的原理在 <a href="/blog/backend/knowledge-cards/message-authentication/" data-link-title="Message Authentication" data-link-desc="兩個系統用共享密鑰互相呼叫時，用來判斷驗證值保護到什麼範圍、撤銷粒度落在哪一層">Message Authentication</a>。下方反覆出現的<strong>驗證素材</strong>指的是進入計算的那串內容——哪些欄位、以什麼順序、用什麼編碼串接起來。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：驗證素材的定義兩端不一致 / 重放窗口未收斂 / 驗證值的比對方式抵銷機制強度。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>該不該用共享密鑰簽章、與其他機制的取捨 → <a href="../machine-credential-mechanism-selection/">7.34</a></li>
<li>密鑰放在哪一格、被誰拿得到 → <a href="../cryptographic-primitive-selection/">7.28</a></li>
<li>密鑰的保存與輪替節奏 → <a href="../secrets-and-machine-credential-governance/">7.6</a></li>
<li>素材規格變更時的相容判斷（本章不談版本策略，只到「規格要雙方共同確認」為止）→ <a href="/blog/backend/11-api-design/backward-compatibility-discipline/" data-link-title="11.6 向後相容的變更紀律" data-link-desc="哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律">11.6 向後相容的變更紀律</a> 的抽象層紀律</li>
</ul>
<p>out-of-scope 的議題直接跳到對應章節。</p>
<h2 id="從本章到實作">從本章到實作</h2>
<p>本章是 routing layer，沿兩條 chain 進入 implementation：</p>
<ul>
<li><strong>Mechanism</strong>：問題節點表「前置控制面」欄的連結進知識卡，看該控制的機制、邊界與適用條件。</li>
<li><strong>Delivery</strong>：「交接路由」欄位指向 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a>、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台</a>、<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性</a>、<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理</a>。重放窗口那一列走 04，因為拒絕原因分不分得開是監控設計的問題。</li>
</ul>
<p>兩條 chain 完成判準與模組級 chain 規格見 <a href="../#%e5%be%9e%e7%ab%a0%e7%af%80%e5%88%b0%e5%af%a6%e4%bd%9c%e7%9a%84-chain">從章節到實作的 chain</a>。</p>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
          <th>交接路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗證素材定義不一致</td>
          <td>兩端各自解讀欄位順序、單位與空值處理</td>
          <td>對接失敗且錯誤訊息無法定位</td>
          <td><a href="/blog/backend/knowledge-cards/message-authentication/" data-link-title="Message Authentication" data-link-desc="兩個系統用共享密鑰互相呼叫時，用來判斷驗證值保護到什麼範圍、撤銷粒度落在哪一層">message-authentication</a>、<a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">api-contract</a></td>
          <td><code>05</code></td>
      </tr>
      <tr>
          <td>重放窗口未收斂</td>
          <td>驗證值涵蓋時間戳但接收端未檢查新鮮度</td>
          <td>攔截到的請求可無限重放</td>
          <td><a href="/blog/backend/knowledge-cards/replay-attack/" data-link-title="Replay Attack" data-link-desc="攔截到的合法請求被原封不動再送一次時，用來判斷哪一層該負責擋、以及擋不住會發生什麼">replay-attack</a>、<a href="/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">idempotency-key</a></td>
          <td><code>04 + 06 + 08</code></td>
      </tr>
      <tr>
          <td>比對方式抵銷強度</td>
          <td>驗證值用一般字串相等運算比對</td>
          <td>回應時間洩漏吻合前綴的長度</td>
          <td><a href="/blog/backend/knowledge-cards/timing-attack/" data-link-title="Timing Attack" data-link-desc="比對密鑰、token 或簽章的程式碼要判斷是否會由執行時間洩漏資訊時的依據">timing-attack</a></td>
          <td><code>05</code></td>
      </tr>
  </tbody>
</table>
<h2 id="判讀流程">判讀流程</h2>
<ol>
<li>先判這個端點需不需要時間軸的保護：重送一次會不會產生第二次副作用。不會的純查詢端點到這裡就結束，剩下兩項仍然要做。</li>
<li>需要的話定窗口長度。第一題是對方的時間戳在事件產生時簽還是每次送出時簽，它決定下界怎麼算；換算與上下界交叉時的處置見下方「重放窗口的收斂條件」。</li>
<li>接著確認素材規格是雙方共同確認過的一份，而不是各自一份文件。缺的話在第一次對接時補。自助式的供應商沒有那個對話（註冊完就上線、沒有窗口可談），這時退到單方規格：把實際收到的請求原樣存下幾筆當成規格的依據、據此寫回歸測試，並在文件裡標明依據是實測而非對方的文件。這樣做的差別在對方改了東西時測試會紅，而不是等到驗簽開始失敗才發現。</li>
<li>再查程式碼裡比對驗證值那一行用的是不是等時比較函式。</li>
<li>最後把窗口長度、素材規格、拒絕原因的分類與第 4 步的檢查結果寫下來。第 1 步判定為不需要時同樣要寫，記的是判定依據（這個端點重送不產生第二次副作用）——否則判定過而判定不需要，與根本沒判過，在產物上看起來一樣。自己是推送方時這幾項屬於對外契約的內容；自己是接收方時對外契約在對方手上，這幾項落在自己的整合紀錄與監控設定裡。兩種情形共同的最低要求是拒絕原因分得開，事件當下才查得出方向。</li>
</ol>
<h2 id="驗證素材的對齊成本">驗證素材的對齊成本</h2>
<p>素材定義不一致是簽章對接裡反覆發生的耗時來源，特徵是<strong>失敗訊號不具指向性</strong>：驗證值對不起來時只會得到一個布林值，看不出是欄位少了一個、順序反了、還是時間戳的單位是秒而對方送的是毫秒。</p>
<p>這個特徵決定了它的成本結構：耗時不隨團隊能力下降，而是隨介面調整次數重複發生。每一次新增欄位、改變編碼、調整空值處理，兩端都要重新對齊一次，而每一次對齊都要付同樣的排錯成本。因此把素材規格寫進雙方共同的書面契約，這筆投資在整合的生命週期裡會被攤提多次——而寫進去的時機在第一次對接時成本最低，那時兩邊的人都還在同一個對話裡。</p>
<p>規格要涵蓋的項目、可在本機單方完成的診斷手法（先讓兩端各自對同一份輸入算一次，比對中間值而非最終值）與對接的檢查順序見 <a href="/blog/work-log/hmac_signature_field_alignment/" data-link-title="HMAC 簽章對接：對不上的是輸入定義、用確定性反推它落在哪一端" data-link-desc="兩端 HMAC 算不出同一個值時要逐項核對的輸入定義，以及用簽章反推輸入、判斷問題落在哪一端的除錯手法。">HMAC 簽章對接</a>。</p>
<p><strong>驗證素材定義不一致</strong>要有兩個團隊各自實作才會發生，因此它的密度與對接對象數量成正比。跨組織整合、集團內跨產品線、以及自己的服務對接第三方的 webhook 都落在這裡。識別特徵是雙方各有一份描述簽章怎麼算的文件，而沒有一份是雙方共同確認過的。</p>
<h2 id="重放窗口的收斂條件">重放窗口的收斂條件</h2>
<p>哪些端點需要這一步，判準是重送一次會不會產生第二次副作用：轉帳、下單、發送通知、狀態轉移都會，純查詢端點不會。接收外部 webhook 的端點風險最高——請求來自自己控制範圍之外，攻擊者可能就在傳輸路徑上，而 webhook 的重試機制本身就會產生大量重複請求，讓惡意重放藏在正常重試裡。</p>
<p>時間軸條件（驗證值涵蓋時間戳、接收端檢查新鮮度）擋住窗口外的重送，窗口內的重複由識別值去重承接，兩者的分工見 <a href="/blog/backend/knowledge-cards/replay-attack/" data-link-title="Replay Attack" data-link-desc="攔截到的合法請求被原封不動再送一次時，用來判斷哪一層該負責擋、以及擋不住會發生什麼">Replay Attack</a>。這個節點的特殊之處在於缺的那一步從來沒有被排進工作項：驗簽做完了、測試綠了、上線了，而「還要比對時間戳與識別值」這件事沒有出現在任何一張清單上，所以它不是做失敗、是沒有人想到要做。</p>
<p>它的失敗長這樣：webhook 接收端照對方文件實作了簽章驗證，驗簽通過就代表這個請求出自對方，當下沒有人覺得還缺什麼——時間戳與識別值都在 payload 裡，兩者都沒有人比對。某次對方那端重試堆積之後，同一筆扣款被處理了好幾次。查起來每一筆都通過驗簽、每一筆的內容都合法，監控上是一串正常請求；而 webhook 本來就會重試，重複請求與惡意重放在日誌裡沒有任何欄位分得開。</p>
<p>補救的順序由重複落在哪一側決定，而這個案例落在窗口內側：重試堆積發生在幾秒到幾分鐘之內，新鮮度檢查放行它是正確行為，擋得住它的是識別值去重。所以先建去重儲存，再補時間戳的新鮮度檢查把窗口外的重送一併擋掉——兩道控制的分工見上一段。已經發生的重複副作用要逐筆對帳回滾，那部分的工作量由這個缺口存在了多久決定。</p>
<p>窗口長度是對接階段就要定的參數，兩端都能換算成具體的量。下界由對方的時間戳在什麼時候簽決定：送出時簽的話每次重試各自帶新的時間戳，下界就是量測到的 <a href="/blog/backend/knowledge-cards/clock-skew/" data-link-title="Clock Skew" data-link-desc="跨機器比較時間才成立的機制（時效窗口、憑證有效期、事件排序）在決定容忍值時的判斷依據">時鐘偏移</a> 上界加餘裕，落在秒到分鐘級；產生時簽的話整串重試共用同一個時間戳，下界要改取時鐘偏移上界與對方最大重試退避間隔的較大者，而後者通常大上幾個數量級——退避超過窗口的那些合法重試會被判成過期，日誌上與攻擊分不開。這一題要向對方確認，退避表查不到時當作產生時簽並取對方公告的重試總時長，<a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">指數退避</a> 是它最常見的形狀。</p>
<p>上界由去重的儲存量反推：窗口內的已處理識別值都要留著，請求速率乘上窗口長度就是要保留的筆數。這個數字撐不住時要動的是儲存形態而非窗口——存識別值的雜湊而非原值、讓儲存自己按存活時間淘汰——因為把窗口縮到下界以下會開始拒絕合法的重試，那與攻擊在日誌上分不開。兩端交叉到怎麼調都撐不住時，剩下的路是接受窗口外的重複由業務層的冪等承接，收斂點見 <a href="/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">Idempotency Key</a>。涉及金流或不可逆動作的端點直接取下界，不必在區間裡挑。</p>
<p><strong>重放窗口未收斂</strong>出現在接收外部推送的端點，尤其是那些照著對方文件實作驗簽就上線的整合。這一格的成因與其餘節點不同：它不是做錯，是做完之後少做一步，因此它的出現率與驗簽實作的正確性無關——寫得越乾淨的實作越容易停在這裡，因為驗簽那一段看起來已經完成了。識別特徵是 payload 裡帶著時間戳與識別值，而接收端的程式碼裡找不到比對它們的地方。</p>
<h2 id="比對方式抵銷機制強度">比對方式抵銷機制強度</h2>
<p>重算出的值要用等時比較來比對。一般的字串相等運算在第一個不吻合的位元組就回傳，回應時間因此洩漏吻合前綴的長度，攻擊者可以逐位元組推出正確的驗證值，收斂點見 <a href="/blog/backend/knowledge-cards/timing-attack/" data-link-title="Timing Attack" data-link-desc="比對密鑰、token 或簽章的程式碼要判斷是否會由執行時間洩漏資訊時的依據">Timing Attack</a>。</p>
<p><strong>比對方式抵銷強度</strong>出現在自己動手實作驗簽的服務。用對方提供的 SDK 多半不會踩到，因為那一行藏在函式庫裡（多半而非一定——SDK 沒覆蓋到的框架仍要自己寫）；照文件自己寫的會踩到，因為文件多半只說明怎麼算出那個值、不說怎麼比對它。識別動作是查程式碼裡比對驗證值那一行。</p>
<p>各語言都有現成的等時比較函式（<code>hash_equals</code>、<code>compare_digest</code>、<code>ConstantTimeCompare</code> 這一類），判別方式是查程式碼裡比對驗證值那一行用的是不是那個函式。函式取用不到的環境還有第二條路：把收到的值與自己算出的值各再做一次 HMAC（用一把當場產生的隨機密鑰）之後比對，攻擊者無法預測比對的是什麼，時間差因此不再洩漏前綴長度。它與其餘節點的差別在它完全在自己這一側，不必與對方協調。</p>
<h2 id="常見風險邊界">常見風險邊界</h2>
<ul>
<li>驗證素材沒有雙方共同的書面規格時，代表對接成本會在每次介面調整時重新發生，而那筆成本每次都由兩邊同時付。</li>
<li>端點會產生第二次副作用而時間戳與識別值都沒有被比對時，保護只剩「這個請求出自對方」，攔截到的請求可以無限重放。</li>
<li>窗口長度取得比量測到的時鐘偏移還短時，正常請求會在漂移時被拒，而那個拒絕在日誌裡與攻擊的表徵相同。</li>
<li>監控把「驗證失敗」併成一個計數時，事件當下無法從監控判斷該往哪個方向查。三者指向不同：驗證值不符指向規格或密鑰不一致、時間戳過期指向時鐘偏移、識別值重複則是三者裡唯一需要區分惡意與正常重試的一種——它的絕大多數來源是對方的重試，所以不能只看計數，要查得到來源位址與時間分布。</li>
<li>對方的文件與實際送出的內容不一致而對方不願修文件時，處置要往契約層走：把實測結果寫成雙方確認過的附件，並把自己這一側的實作依據記成「依實測而非依文件」。對方連附件都不給時這條整合帶著一個沒有書面依據的假設上線，期限與重評估條件走 <a href="../security-governance-exception-and-tripwire/">7.14 資安治理例外與 Tripwire</a>。</li>
</ul>
<h2 id="案例觸發參考">案例觸發參考</h2>
<ul>
<li>簽章方案在對外契約上的實際形態： <a href="/blog/backend/11-api-design/cases/webhook-stripe-delivery-contract/" data-link-title="11.C60 Stripe webhooks：at-least-once 加簽章、明文要求 consumer 冪等與不依賴順序" data-link-desc="webhook 對外承諾的教科書樣本：三天重試、重複投遞、no-ordering、簽章驗證的責任在同一頁明文轉移給 consumer">Stripe webhook 投遞契約</a></li>
<li>硬編碼憑證與固定認證路徑： <a href="../red-team/cases/edge-exposure/usaherds-cve-2021-44207-hardcoded-credential/">USAHERDS 2021</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游（該不該用共享密鑰簽章）：<a href="../machine-credential-mechanism-selection/">7.34 機器憑證的機制選型</a></li>
<li>機制原理與能力上限：<a href="/blog/backend/knowledge-cards/message-authentication/" data-link-title="Message Authentication" data-link-desc="兩個系統用共享密鑰互相呼叫時，用來判斷驗證值保護到什麼範圍、撤銷粒度落在哪一層">Message Authentication</a></li>
<li>素材的逐項清單與診斷手法：<a href="/blog/work-log/hmac_signature_field_alignment/" data-link-title="HMAC 簽章對接：對不上的是輸入定義、用確定性反推它落在哪一端" data-link-desc="兩端 HMAC 算不出同一個值時要逐項核對的輸入定義，以及用簽章反推輸入、判斷問題落在哪一端的除錯手法。">HMAC 簽章對接</a></li>
<li>密鑰放在哪一格、被誰拿得到：<a href="../cryptographic-primitive-selection/">7.28 密碼學原語選型</a></li>
<li>密鑰怎麼交到對方手上：<a href="../machine-credential-issuance/">7.32 機器憑證的配發</a></li>
<li>密鑰的輪替與回收節奏：<a href="../secrets-and-machine-credential-governance/">7.6 秘密管理與機器憑證治理</a></li>
<li>去重儲存與識別值的設計：<a href="/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">Idempotency Key</a></li>
<li>素材規格新增欄位時兩端怎麼同步：本站尚無專章；通用的相容紀律見 <a href="/blog/backend/11-api-design/backward-compatibility-discipline/" data-link-title="11.6 向後相容的變更紀律" data-link-desc="哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律">11.6 向後相容的變更紀律</a>，在那之前的最小做法是把新欄位設成可選、兩端各自先支援「有或沒有都算對」一段時間，再切成必填</li>
</ul>
]]></content:encoded></item></channel></rss>