7.33 委任型憑證:關係寫進憑證,還是留給驗證方拼湊
第二個下游服務接上來的時候,「這個系統獲准代表這位使用者」是在哪裡被檢查的。
本章的責任是把「A 代表 B」這層關係的表達方式拆成可判讀的選型問題,讓兩張憑證(使用者的憑證加呼叫系統的憑證各一張)與一張委任型憑證各自的成本、撤銷路徑與驗證責任清楚。
本章涵蓋與不涵蓋
本章聚焦憑證形態的選擇與驗證方的檢查責任。代理這件事在授權層要配什麼(發起的理由、有效時窗、紀錄裡的雙重歸屬、多跳可不可遞移)見 7.2 代理操作的授權邊界;憑證屬於哪一層身分維度見 7.29 API 認證的信任邊界分層。另有一個詞要先分開:本章的「委任」(delegation)是一個系統代表某個特定的人去做事,而 7.31 的「委派」是把登入這件事整個交給外部的身分提供者。兩者的中文只差一個字、指涉完全不同——本章不處理登入從哪裡來。兩處與本章問的是不同的問題:那裡問授權可不可遞移,這裡問憑證要不要合併成一張。
本章 threat scope
In-scope:委任關係由各驗證方各自認定 / 換成委任型後撤銷粒度未實際成立 / 交換出來的憑證失去雙重歸屬 / 代理後的權限取聯集 / 委任型專屬的驗證檢查未做。
Out-of-scope(路由到他章):
Reader 對 in-scope 列表的 specific threat 應該能反向 trace 到本章問題節點;out-of-scope 議題請直接跳到對應章節。
從本章到實作
本章是 routing layer,沿兩條 chain 進入 implementation:
兩條 chain 完成判準與模組級 chain 規格見 從章節到實作的 chain。
關係由誰確認一次,決定它會不會分岔
選型的核心責任是決定「這個系統獲准代表這個人」由誰認定。兩張憑證的做法把這個認定交給每一個驗證方:下游服務同時收到使用者的憑證與呼叫系統的憑證,然後自己決定兩者都有效算不算數。委任型憑證把認定移到發行時完成一次,驗證方讀出來的是已經被確認過的關係。
分岔就發生在「自己決定」這一步,而且它是安全語意上的分岔。一個下游服務可能認為兩張都有效就放行——那等於任何獲授權的系統都能代表任何使用者;另一個下游服務可能另外查一張授權表,確認這個系統確實被允許代表這位使用者。兩種實作在功能測試裡都會通過,因為正常請求兩邊都放行;差別只在攻擊者手上有一張合法的系統憑證與一個任意使用者識別值的時候。
本章是這個選型的落點:7.29 交出「預設用兩張」這個結論,換過去的判準、機制與驗證責任在這裡。
這件事在只有一個驗證方時看不出來,因為只有一種拼法就沒有不一致可言。所以判斷要不要換,看的是拼法規則由誰保證一致,而不是代理這個需求本身有多重要。驗證方的數量是這個問題最常見的代理指標——多一個驗證方就多一種拼法。它涵蓋不到的是另一種形態:驗證方只有一個、但那一個在別的組織,此時拼法只能靠對外契約約束,而契約的維護成本高於發行方一次認定。單一驗證方而屬外部組織的,與多驗證方落在同一側。
驗證方數量之外另有一個訊號:稽核要求。要求查得出「哪個系統代表哪個人做了這件事」時,兩張憑證的做法要在每個驗證方各自把兩份身分都寫進紀錄,而漏寫其中一份沒有任何訊號——紀錄看起來完整,只是把代理操作記成了那個人自己的操作。委任型憑證讓雙重歸屬變成憑證的內容,紀錄照抄即可。這一格與 7.2 代理操作的授權邊界 對稽核紀錄的要求是同一件事在憑證層的支撐。
換過去消除的是「這個關係存不存在」的分岔——兩張憑證時每個驗證方可能接受一個沒有任何人授權過的代理關係,委任型讓那件事必須經過發行方確認一次。分岔本身沒有全部消失,它降級成次級判斷(要不要信任這個發行方、act 缺席怎麼辦、巢狀幾層可接受),而那幾項見下方「驗證方要檢查什麼」。同時被集中過去的還有「這個關係給多少」——換出來的範圍由發行方算,而標準沒有規定它必須算(見下方「權限範圍是交集」)。發行方不算的時候,驗證方失去的是自行核對範圍的能力,那一格比兩張憑證更糟。
委任與冒用是交換出來的兩種結果
換成委任型憑證會引進一個在兩張憑證那條路上不存在的角色:發行方。兩張憑證的做法裡沒有人需要在中間認定關係,各方各自驗自己收到的那一張;委任型把認定集中到一次,因此必須有一方負責做那次認定並簽出憑證,那一方就是下面說的授權伺服器。
標準形態是 OAuth 2.0 Token Exchange(RFC 8693):呼叫方帶著被代理者的憑證(subject_token)與自己的身分(actor_token,或由呼叫端本身的 client 認證承載——標準把 actor_token 列為選填)向授權伺服器(發行憑證的那一方,OAuth 標準裡的 authorization server)換一張新的憑證。發行方在這一步做兩件事——確認這個代理關係獲准成立,以及決定換出來的憑證長什麼樣。
換出來的結果有兩種語意,差別在有沒有保留代理方:
委任(delegation)保留兩個主體。憑證的主體仍然是被代理的那個人,另有一個 act 欄位記錄代理方是誰。下游看得到「這是誰、由誰代為執行」,稽核鏈完整。act 可以巢狀,多跳代理因此能保留整條鏈,這正是 7.2 收斂條件掛在紀錄能力上 的那個機制。
冒用(impersonation)只留下被代理的那個人。下游收到的憑證與那個人自己登入拿到的沒有差別,因此不必理解代理這個概念。代價是稽核鏈在這一跳斷掉:事後查得到那個人做了什麼,查不到這是代理。這裡的冒用是發行方核可的一種憑證形態,與攻擊者假冒他人身分是不同的事——差別在有沒有經過發行方那一次確認。
選型的預設是委任。冒用的適用條件是下游無法被修改成理解 act(第三方服務、封閉的既有系統),而且該操作的歸屬爭議成本可以接受。落在冒用的整合要把歸屬補在別的地方——發行端的交換紀錄是唯一不依賴代理方自證的那個位置(代理方自己的出向紀錄也知道它代表誰呼叫,但那份紀錄由被查的一方保管),它的保存期限因此要對齊稽核要求,而不是對齊一般的存取日誌。保存期限該對齊什麼、證據要留哪些欄位見 7.7 稽核追蹤與責任邊界。
發行方憑什麼確認這個關係獲准成立,是授權層的問題。標準留了一個掛鉤:被代理者的憑證裡可以帶 may_act,宣告哪一方獲准代表它。這個欄位由誰寫入、依據什麼規則寫入,回到 7.2 的授權設計。
撤銷粒度要另外成立
換成委任型憑證之後撤銷粒度變細,這句話有前提,而前提常常沒有被檢查。
想撤的對象有三種,各自的路徑不同。撤那個人(離職、帳號被盜)要讓他被代理的能力一起停止;撤那個應用(第三方出包、整合下線)要讓它代表所有人的能力一起停止,其他應用照常;撤這個組合(這個應用不再獲准代表這個租戶)在多數既有機制裡沒有直接對應的動作,處置落在授權層——把 may_act 或等價的授權關係撤掉,讓後續交換無法成立。授權模型本身怎麼設計(這類關係放角色還是放資源)本站尚無專章,這一句是它之前的最小做法。
前兩條在「後續還能不能換到新憑證」這件事上自動成立:發行方在交換當下檢查兩邊的狀態,任一邊失效就換不到。真正的落差在已經發出去的憑證。自包含的憑證(驗證所需的判斷材料全寫在憑證裡,驗證方讀完簽章就能放行,不必問發行方)由驗證方自行驗簽,發行方那邊的狀態變化到不了它——那個人被停用之後,他名下已發出的委任憑證在剩餘有效期內照常通過驗證。
要讓撤銷及於已發出的憑證,可用的路徑有兩條:把有效期壓到可接受的殘留時間以內,或讓驗證方在使用時回查發行方的狀態(OAuth 的 token introspection,RFC 7662:驗證方把憑證送回發行方,換回它現在還有效沒有)。第一條的成本落在交換頻率(憑證越短越常換),第二條的成本落在每個請求多一次外部呼叫;回查那條路徑掛掉時要先決定放行還是拒絕,而這個決定同時是可用性與安全性的取捨,收斂點見 token-revocation。兩條都要動到驗證端,所以它與「換成委任型」是兩個獨立的決定,而不是同一個決定的兩個部分。
驗證方要檢查什麼
驗證方的責任是把憑證裡的宣稱轉成可執行的判斷。委任型憑證比一般憑證多了一個主體,因此檢查項也多,以下各項共同決定多出來的那個主體有沒有實際效力,而它們各自失效的形態不同。
接收對象:憑證的 audience(這張憑證是發給哪個服務用的)要與自己相符。缺這項檢查時,發給 A 服務的憑證可以被拿去打 B 服務,而委任關係讓這件事更值得防——同一個代理方在不同服務的授權範圍未必相同。
代理方是誰:讀 act。這一項存在代表這是委任型憑證,紀錄要同時寫入兩個主體。這一項缺席代表拿到的是冒用型憑證,而如果自己的服務本來預期收到委任型,這是設定漂移的訊號而非正常形態。
授權只看最外層的代理方:act 巢狀時,做授權判斷的依據是憑證的頂層宣稱加上 act 指出的當前代理方,內層的歷史代理方只作為紀錄用途。這是標準對驗證方唯一的強制要求(RFC 8693 §4.1),而它擋的正是最容易做錯的兩種實作——拿最內層那個原始代理方做授權,或把整條鏈的權限聚合起來。
巢狀深度:act 的層數就是代理鏈的長度。自己接受幾跳要有明確的上限,而上限的依據是紀錄承載得了幾層——這一條的判準在 7.2。
權限範圍是交集:代理之後的有效權限應該是那個人本來有的、與那個應用獲准代理的,兩者的交集。取聯集會讓代理成為提權管道:一個權限很低的使用者,透過一個權限很高的應用去操作,結果拿到應用的權限。交集只能在發行方算,因為只有那裡同時看得到兩邊的權限;而標準沒有規定它必須被算——RFC 8693 把換出來的憑證範圍留給發行方的政策,只建議用 scope 限制濫用的範圍(§1.1、§5)。這一點正是驗證方要實測的理由:拿一個低權限使用者搭配高權限應用換一張憑證出來,看範圍欄位落在哪一邊。實測結果要寫進上一步那份廠商能力紀錄,並在發行方升版之後重跑一次——這是一次會過期的判定,而它過期的時候沒有任何訊號。
有效期:委任憑證的壽命要短於授權關係本身,而不是短於換進去的那張憑證。標準刻意讓兩者不緊耦合——輸入憑證續期不會反映到輸出憑證上(RFC 8693 §2.1),所以拿一張五分鐘的一次性斷言換一張撐完整批作業的委任憑證是合理的。要盯的對象是授權關係:那個人的登入狀態或那個應用的代理授權結束之後,代理就不該繼續。輸入憑證本身即是授權關係的載體時(長效的 session token),這一條才退化成「早於輸入憑證」。
判讀流程
- 先數驗證方。只有一個時留在兩張憑證,並把拼法寫成該驗證方的明確規則(兩張都有效之外還查什麼)。
- 驗證方在兩個以上、其中任一個屬外部組織、或稽核要求查得出雙重歸屬時,進入下一步的成本 gate。
- 確認發行端具備交換能力,而這要分三個問題確認,且三題的取得方式不同:支不支援交換看得到答案(廠商的端點文件或 OAuth 探索文件);換出來的憑證帶不帶
act、範圍是取交集還是照抄輸入,文件通常不寫,要換一張出來看——第三題的實測方法在下方「權限範圍是交集」那一項。只答第一題就導入的常見結果是拿到冒用型,直接落進下方「交換結果失去雙重歸屬」那個節點。既有身分提供者三題都過時成本落在設定;發行端要從零建置時成本通常超過這個決定本身的收益,此時的替代做法是把兩張憑證的拼法定成跨服務共用的規格。三題的答案要寫下來並註明問的是哪個版本——它們是會變的廠商能力,而下一次有人重新評估時,重問一輪的成本遠高於讀一次紀錄。 - 決定委任還是冒用。下游改得動就用委任;改不動時用冒用,並把雙重歸屬留在發行端的交換紀錄。
- 確認撤銷路徑實際成立,而驗收要用「停用前先換發、停用後再拿那一張去打」的順序測過並留下紀錄——照自然順序測(先停用再換一次)會假通過,理由見下方微案例。驗收要記下拒絕的原因欄位,並確認測試在那張憑證的有效期內完成:有效期壓短之後,「被拒絕」同時是撤銷生效與單純過期的表徵,而分不出這兩者的驗收等於沒做。選擇的依據是可接受的殘留窗口對上交換頻率的成本上限:殘留窗口容納得下憑證有效期時把有效期壓短就夠;窗口短到交換頻率撐不住時才走回查,代價是驗證端從此依賴發行方的可用性,那條依賴要接進 06 可靠性 的降級設計。
- 最後把驗證方的檢查項寫進對外契約——落點是介面規格裡描述認證方式的那一段,以及給整合方的對接文件,兩者要一致。契約的責任邊界見 api-contract,已上線的整合要改檢查項走 11.6 向後相容的變更紀律。
問題節點(案例觸發式)
| 問題節點 | 判讀訊號 | 風險後果 | 前置控制面 | 交接路由 |
|---|---|---|---|---|
| 委任關係由各驗證方認定 | 下游服務各自決定「兩張都有效」算不算獲准代理 | 任一寬鬆的驗證方讓合法系統可代理任何人 | authorization、trust-boundary | 05 + 08 |
| 撤銷粒度未實際成立 | 已發出的委任憑證在主體失效後仍通過驗證 | 停用動作與代理停止之間有殘留窗口 | token-revocation、session-invalidation | 06 + 08 |
| 交換結果失去雙重歸屬 | 下游紀錄只有被代理者,看不出是代理 | 事後無法回答哪個系統代表哪個人操作 | audit-log、non-repudiation | 08 |
| 代理後的權限取聯集 | 低權限使用者透過高權限應用取得應用的權限 | 代理路徑成為提權管道 | least-privilege、authorization | 06 + 08 |
| 委任型檢查未做 | audience 未比對,或授權判斷取內層代理方 | 憑證可跨服務使用,代理鏈成為權限累加路徑 | authorization-scope、api-contract | 05 + 06 |
問題節點出現在什麼樣的系統
上表的訊號要等機制上線才看得到。設計階段對照的是下面這幾種形態。
委任關係由各驗證方認定出現在下游服務逐個長出來的系統。第一個下游接上時只有一種拼法,也就沒有一致性問題;第二個下游由另一個團隊實作,參考的是文件而不是第一個下游的程式碼,而文件多半只說明兩份憑證各自怎麼驗。識別特徵是問得出「代理關係在哪裡被檢查」的人只有原始設計者。
撤銷粒度未實際成立出現在剛從兩張憑證換成委任型的系統——換過去的動機通常就是撤銷粒度,因此團隊會認為這件事已經解決,而有效期與回查是另一組決定,它們在遷移的工作清單上不會自然出現。
交換結果失去雙重歸屬出現在下游包含第三方或封閉系統的整合,因為冒用正是為了讓那些下游能收。
代理後的權限取聯集出現在授權判斷散落在各服務的架構:交集要有一個地方同時看得到兩邊的權限,權限分散時那個地方不存在。
委任型檢查未做出現在把委任型憑證當成一般憑證接的下游——驗證程式碼沿用既有的那一套,而既有那一套沒有 act 這個概念。
撤銷粒度未實際成立的後果在這張表裡最不直觀:它的表徵是驗收做過、而驗收走的路徑與失效的路徑不相交。
它的失敗長這樣:整合從兩張憑證換成委任型,理由是要能單獨撤掉某個使用者而不影響整條整合。上線後做了驗收——把測試帳號停用,再用那個應用代表它呼叫一次,回應是拒絕,驗收通過。
缺口從上線第一天就在,而它第一次產生後果是在某次事件處置當天:某個帳號被判定遭盜用而停用,事後要回答「停用之後還有沒有動作」,紀錄顯示接下來的一段時間內仍有以該身分執行的操作。
驗收當時會通過,是因為測試用的是一次新的交換:停用之後換不到新憑證,這條路徑確實生效。而已經發出去的憑證走的是另一條路徑,驗收沒有碰到它——要碰到它必須在停用之前先換一張憑證留著,停用之後再用那一張。這個步驟在自然的驗收流程裡不會出現,因為它要求測試者刻意保留一個舊狀態。
止血是把有效期壓短,而那要改的是發行端的設定與驗證端的容忍度,兩邊都要動——事件當天做這件事的難度,遠高於遷移當時把它列進清單。
常見風險邊界
委任設計在以下情況下不再是功能議題。
- 代理關係的檢查點列不出來時,代表這件事沒有被任何一方負責,而每個驗證方各自的實作都通過測試。收斂的第一步是拿本章「驗證方要檢查什麼」逐項對每個驗證方核對過去,核對不過的那幾項就是缺口清單。
- 主體被停用之後,已發出的委任憑證的殘留時間超出事件處置能接受的窗口時,撤銷粒度只存在於帳面上。
- 交換出來的憑證失去
act而發行端的交換紀錄保存期短於稽核要求時,雙重歸屬在兩處同時消失。保存期限的定法見 7.7 稽核追蹤與責任邊界。 - 代理後的權限範圍與被代理者本人的權限範圍不同時,要能指出差異來自哪一邊;指不出來代表交集沒有被計算。
- 多跳代理的層數超過紀錄能承載的層數時,收斂條件見 7.2。
案例觸發參考
- 代理操作濫用的失效樣式: 代理操作濫用
- 代理會話的上下文混層: 代理會話上下文混層
- 第三方 token 成為內部入口: GitHub OAuth 2022
- token 洩漏後的存取延續: Slack 2022
下一步路由
- 代理在授權層要配什麼、多跳可不可遞移:7.2 代理操作的授權邊界
- 這張憑證在身分維度裡的位置與混層訊號:7.29 API 認證的信任邊界分層
- 代理方本身那把機器憑證怎麼核發與交付:7.32 機器憑證的配發
- 憑證上線後的輪替與回收:7.6 秘密管理與機器憑證治理
- 稽核紀錄要留什麼欄位:7.7 稽核追蹤與責任邊界
- 驗證項寫進對外契約:api-contract;已上線的整合要改檢查項時走 11.6 向後相容的變更紀律
- 事件發生後的止血與回復:8.x 止血與回復策略