7.35 簽章對接的驗證收斂:驗簽通過之後還缺哪一塊
驗簽通過之後還有三件事要做,而它們在功能測試裡都不會出現。
這一章要交出去的是三樣東西:一份雙方確認過的驗證素材規格、一個換算得出上下界的窗口長度、以及一組分得開的拒絕原因。第一樣決定對接要花多久,第二樣決定驗簽通過之後還擋不擋得住重送,第三樣決定事件當天查得出方向。另有一項不必等對方就能修完:比對驗證值那一行用對函式。
本章涵蓋與不涵蓋
本章的三個收斂條件涵蓋範圍不同:驗證素材的對齊是共享密鑰簽章專屬,而重放窗口對任何接收外部推送的端點都成立(與用哪一種機制無關),比對方式對任何做等值比對的秘密都成立(API key 的比對有一模一樣的時間洩漏)。機制本身還沒選的讀者從 7.34 機器憑證的機制選型 進來;這個機制屬於哪一類原語、密鑰放在哪一格見 7.28 密碼學原語選型;密鑰的保存與輪替屬治理層,見 7.6 秘密管理與機器憑證治理。本章聚焦選定之後的收斂條件,機制本身的原理在 Message Authentication。下方反覆出現的驗證素材指的是進入計算的那串內容——哪些欄位、以什麼順序、用什麼編碼串接起來。
本章 threat scope
In-scope:驗證素材的定義兩端不一致 / 重放窗口未收斂 / 驗證值的比對方式抵銷機制強度。
Out-of-scope(路由到他章):
- 該不該用共享密鑰簽章、與其他機制的取捨 → 7.34
- 密鑰放在哪一格、被誰拿得到 → 7.28
- 密鑰的保存與輪替節奏 → 7.6
- 素材規格變更時的相容判斷(本章不談版本策略,只到「規格要雙方共同確認」為止)→ 11.6 向後相容的變更紀律 的抽象層紀律
out-of-scope 的議題直接跳到對應章節。
從本章到實作
本章是 routing layer,沿兩條 chain 進入 implementation:
- Mechanism:問題節點表「前置控制面」欄的連結進知識卡,看該控制的機制、邊界與適用條件。
- Delivery:「交接路由」欄位指向 04 可觀測性、05 部署平台、06 可靠性、08 事故處理。重放窗口那一列走 04,因為拒絕原因分不分得開是監控設計的問題。
兩條 chain 完成判準與模組級 chain 規格見 從章節到實作的 chain。
問題節點(案例觸發式)
| 問題節點 | 判讀訊號 | 風險後果 | 前置控制面 | 交接路由 |
|---|---|---|---|---|
| 驗證素材定義不一致 | 兩端各自解讀欄位順序、單位與空值處理 | 對接失敗且錯誤訊息無法定位 | message-authentication、api-contract | 05 |
| 重放窗口未收斂 | 驗證值涵蓋時間戳但接收端未檢查新鮮度 | 攔截到的請求可無限重放 | replay-attack、idempotency-key | 04 + 06 + 08 |
| 比對方式抵銷強度 | 驗證值用一般字串相等運算比對 | 回應時間洩漏吻合前綴的長度 | timing-attack | 05 |
判讀流程
- 先判這個端點需不需要時間軸的保護:重送一次會不會產生第二次副作用。不會的純查詢端點到這裡就結束,剩下兩項仍然要做。
- 需要的話定窗口長度。第一題是對方的時間戳在事件產生時簽還是每次送出時簽,它決定下界怎麼算;換算與上下界交叉時的處置見下方「重放窗口的收斂條件」。
- 接著確認素材規格是雙方共同確認過的一份,而不是各自一份文件。缺的話在第一次對接時補。自助式的供應商沒有那個對話(註冊完就上線、沒有窗口可談),這時退到單方規格:把實際收到的請求原樣存下幾筆當成規格的依據、據此寫回歸測試,並在文件裡標明依據是實測而非對方的文件。這樣做的差別在對方改了東西時測試會紅,而不是等到驗簽開始失敗才發現。
- 再查程式碼裡比對驗證值那一行用的是不是等時比較函式。
- 最後把窗口長度、素材規格、拒絕原因的分類與第 4 步的檢查結果寫下來。第 1 步判定為不需要時同樣要寫,記的是判定依據(這個端點重送不產生第二次副作用)——否則判定過而判定不需要,與根本沒判過,在產物上看起來一樣。自己是推送方時這幾項屬於對外契約的內容;自己是接收方時對外契約在對方手上,這幾項落在自己的整合紀錄與監控設定裡。兩種情形共同的最低要求是拒絕原因分得開,事件當下才查得出方向。
驗證素材的對齊成本
素材定義不一致是簽章對接裡反覆發生的耗時來源,特徵是失敗訊號不具指向性:驗證值對不起來時只會得到一個布林值,看不出是欄位少了一個、順序反了、還是時間戳的單位是秒而對方送的是毫秒。
這個特徵決定了它的成本結構:耗時不隨團隊能力下降,而是隨介面調整次數重複發生。每一次新增欄位、改變編碼、調整空值處理,兩端都要重新對齊一次,而每一次對齊都要付同樣的排錯成本。因此把素材規格寫進雙方共同的書面契約,這筆投資在整合的生命週期裡會被攤提多次——而寫進去的時機在第一次對接時成本最低,那時兩邊的人都還在同一個對話裡。
規格要涵蓋的項目、可在本機單方完成的診斷手法(先讓兩端各自對同一份輸入算一次,比對中間值而非最終值)與對接的檢查順序見 HMAC 簽章對接。
驗證素材定義不一致要有兩個團隊各自實作才會發生,因此它的密度與對接對象數量成正比。跨組織整合、集團內跨產品線、以及自己的服務對接第三方的 webhook 都落在這裡。識別特徵是雙方各有一份描述簽章怎麼算的文件,而沒有一份是雙方共同確認過的。
重放窗口的收斂條件
哪些端點需要這一步,判準是重送一次會不會產生第二次副作用:轉帳、下單、發送通知、狀態轉移都會,純查詢端點不會。接收外部 webhook 的端點風險最高——請求來自自己控制範圍之外,攻擊者可能就在傳輸路徑上,而 webhook 的重試機制本身就會產生大量重複請求,讓惡意重放藏在正常重試裡。
時間軸條件(驗證值涵蓋時間戳、接收端檢查新鮮度)擋住窗口外的重送,窗口內的重複由識別值去重承接,兩者的分工見 Replay Attack。這個節點的特殊之處在於缺的那一步從來沒有被排進工作項:驗簽做完了、測試綠了、上線了,而「還要比對時間戳與識別值」這件事沒有出現在任何一張清單上,所以它不是做失敗、是沒有人想到要做。
它的失敗長這樣:webhook 接收端照對方文件實作了簽章驗證,驗簽通過就代表這個請求出自對方,當下沒有人覺得還缺什麼——時間戳與識別值都在 payload 裡,兩者都沒有人比對。某次對方那端重試堆積之後,同一筆扣款被處理了好幾次。查起來每一筆都通過驗簽、每一筆的內容都合法,監控上是一串正常請求;而 webhook 本來就會重試,重複請求與惡意重放在日誌裡沒有任何欄位分得開。
補救的順序由重複落在哪一側決定,而這個案例落在窗口內側:重試堆積發生在幾秒到幾分鐘之內,新鮮度檢查放行它是正確行為,擋得住它的是識別值去重。所以先建去重儲存,再補時間戳的新鮮度檢查把窗口外的重送一併擋掉——兩道控制的分工見上一段。已經發生的重複副作用要逐筆對帳回滾,那部分的工作量由這個缺口存在了多久決定。
窗口長度是對接階段就要定的參數,兩端都能換算成具體的量。下界由對方的時間戳在什麼時候簽決定:送出時簽的話每次重試各自帶新的時間戳,下界就是量測到的 時鐘偏移 上界加餘裕,落在秒到分鐘級;產生時簽的話整串重試共用同一個時間戳,下界要改取時鐘偏移上界與對方最大重試退避間隔的較大者,而後者通常大上幾個數量級——退避超過窗口的那些合法重試會被判成過期,日誌上與攻擊分不開。這一題要向對方確認,退避表查不到時當作產生時簽並取對方公告的重試總時長,指數退避 是它最常見的形狀。
上界由去重的儲存量反推:窗口內的已處理識別值都要留著,請求速率乘上窗口長度就是要保留的筆數。這個數字撐不住時要動的是儲存形態而非窗口——存識別值的雜湊而非原值、讓儲存自己按存活時間淘汰——因為把窗口縮到下界以下會開始拒絕合法的重試,那與攻擊在日誌上分不開。兩端交叉到怎麼調都撐不住時,剩下的路是接受窗口外的重複由業務層的冪等承接,收斂點見 Idempotency Key。涉及金流或不可逆動作的端點直接取下界,不必在區間裡挑。
重放窗口未收斂出現在接收外部推送的端點,尤其是那些照著對方文件實作驗簽就上線的整合。這一格的成因與其餘節點不同:它不是做錯,是做完之後少做一步,因此它的出現率與驗簽實作的正確性無關——寫得越乾淨的實作越容易停在這裡,因為驗簽那一段看起來已經完成了。識別特徵是 payload 裡帶著時間戳與識別值,而接收端的程式碼裡找不到比對它們的地方。
比對方式抵銷機制強度
重算出的值要用等時比較來比對。一般的字串相等運算在第一個不吻合的位元組就回傳,回應時間因此洩漏吻合前綴的長度,攻擊者可以逐位元組推出正確的驗證值,收斂點見 Timing Attack。
比對方式抵銷強度出現在自己動手實作驗簽的服務。用對方提供的 SDK 多半不會踩到,因為那一行藏在函式庫裡(多半而非一定——SDK 沒覆蓋到的框架仍要自己寫);照文件自己寫的會踩到,因為文件多半只說明怎麼算出那個值、不說怎麼比對它。識別動作是查程式碼裡比對驗證值那一行。
各語言都有現成的等時比較函式(hash_equals、compare_digest、ConstantTimeCompare 這一類),判別方式是查程式碼裡比對驗證值那一行用的是不是那個函式。函式取用不到的環境還有第二條路:把收到的值與自己算出的值各再做一次 HMAC(用一把當場產生的隨機密鑰)之後比對,攻擊者無法預測比對的是什麼,時間差因此不再洩漏前綴長度。它與其餘節點的差別在它完全在自己這一側,不必與對方協調。
常見風險邊界
- 驗證素材沒有雙方共同的書面規格時,代表對接成本會在每次介面調整時重新發生,而那筆成本每次都由兩邊同時付。
- 端點會產生第二次副作用而時間戳與識別值都沒有被比對時,保護只剩「這個請求出自對方」,攔截到的請求可以無限重放。
- 窗口長度取得比量測到的時鐘偏移還短時,正常請求會在漂移時被拒,而那個拒絕在日誌裡與攻擊的表徵相同。
- 監控把「驗證失敗」併成一個計數時,事件當下無法從監控判斷該往哪個方向查。三者指向不同:驗證值不符指向規格或密鑰不一致、時間戳過期指向時鐘偏移、識別值重複則是三者裡唯一需要區分惡意與正常重試的一種——它的絕大多數來源是對方的重試,所以不能只看計數,要查得到來源位址與時間分布。
- 對方的文件與實際送出的內容不一致而對方不願修文件時,處置要往契約層走:把實測結果寫成雙方確認過的附件,並把自己這一側的實作依據記成「依實測而非依文件」。對方連附件都不給時這條整合帶著一個沒有書面依據的假設上線,期限與重評估條件走 7.14 資安治理例外與 Tripwire。
案例觸發參考
- 簽章方案在對外契約上的實際形態: Stripe webhook 投遞契約
- 硬編碼憑證與固定認證路徑: USAHERDS 2021
下一步路由
- 上游(該不該用共享密鑰簽章):7.34 機器憑證的機制選型
- 機制原理與能力上限:Message Authentication
- 素材的逐項清單與診斷手法:HMAC 簽章對接
- 密鑰放在哪一格、被誰拿得到:7.28 密碼學原語選型
- 密鑰怎麼交到對方手上:7.32 機器憑證的配發
- 密鑰的輪替與回收節奏:7.6 秘密管理與機器憑證治理
- 去重儲存與識別值的設計:Idempotency Key
- 素材規格新增欄位時兩端怎麼同步:本站尚無專章;通用的相容紀律見 11.6 向後相容的變更紀律,在那之前的最小做法是把新欄位設成可選、兩端各自先支援「有或沒有都算對」一段時間,再切成必填