信任邊界
審查者的帳冊: 針對四個跳點中的每一個, 列出強制執行它的機制與在此接受的每一項殘餘風險, 接著是各跳點共用的信任記錄、主機持有策略的帳冊, 以及變更不得倒退的不變量。面向讀者的模型, 連同跳點圖, 在 security.md。
這裡的每一項殘餘風險都已接受並被追蹤。每個條目說明可能發生什麼、前提、什麼限制了它, 以及為何接受。
邊界 1: MCP 用戶端 <-> Rust MCP 伺服器 (stdio, JSON-RPC 2.0)
用戶端程式 (harness) 只有在被准入之後才受信任。這個跳點同時承載協定正確性與授權。
- 協定引擎: 官方的
rmcpSDK 在服務迴圈自身的行長度上限與解析閘門之後提供 JSON-RPC 與 MCP 服務; 信任面的決策見設計依據。未知方法得到-32601, 解析錯誤會回報而非致命, stdout 只承載協定。 - 准入: 在服務任何工具呼叫之前, 伺服器先證明產生它的程序, 再把該身分與信任記錄中已配對的用戶端比對。准入以經證明的錨點 (程式碼簽署者或映像雜湊) 為鍵, 絕不使用用戶端自稱的名稱:
GENKAN_CLIENT_NAME只是日誌標籤。 - 量測什麼: 在 Unix 上, 是
getppid()所指父程序正在執行的映像 (macOS: 經驗證的cdhash加簽署 Team ID; Linux:/proc/<pid>/exe的 SHA-256)。在 Windows 上, 是建立伺服器 stdin 管道的那個程序 (映像 SHA-256 加 Authenticode 發行者作為簽署者), 因為記錄下來的父程序在CreateProcess時可由呼叫者任意指定。 - 態勢: 尚未配對任何用戶端表示不強制准入, 並以 ERROR 等級在每次啟動時記錄。一旦配對了用戶端, 任何不匹配者都失敗即關閉, 包括無法量測的身分與無法讀取的記錄。
- 中繼: 第二個 MCP 伺服器執行個體透過邊界 2 的 socket 接入中介 (broker), 並在其接入訊框中回報自己經證明的父程序, 之所以可信, 是因為中繼連線本身已證明它執行的是這個執行檔。中介以同樣的方式為每個中繼決定准入。
- 撤銷: 准入不是一次決定就算數。信任記錄在工作階段內每個請求之前重新讀取 (中介的逐請求閘門), 閒置連線則按一秒計時器重讀, 因此從任一介面執行的撤銷都會拒絕被撤銷用戶端程式的下一個請求與其重新接入。
- 緊急開關: 閂鎖設定期間, 或記錄無法讀取時, 來自每個用戶端程式的每個
tools/call都在任何路由之前以穩定的BRIDGE_KILLED代碼回應。用戶端程式的連線保持開啟, 讓拒絕以型別化錯誤送達, 而非不透明的斷線。 - 解除: 只能明確進行。CLI 上的
unkill, 或擴充功能在邊界 3 的在場交換之後送出的解除訊框。兩種轉換都會被稽核, 解除還會帶上授權它的認證路徑。
這個跳點的殘餘風險:
- 被證明的一方是產生伺服器的程序, 而非寫入其 stdin 的程序。 stdin 是匿名管道, 沒有核心層的對端憑證, 而管道端點可以被繼承或傳遞, 所以產生者與寫入者無法被證明是同一個。以 pid 為鍵的量測帶有通常的微秒級 pid 重用競態 (在 macOS 上映像仍經簽章驗證)。
- 只量測一次: 父程序在程序啟動時量測, 快取的身分在每個請求上對照信任記錄重新決定。量測之前的重新認父會指向收割程序, 在已配對用戶端之後被拒絕; 產生者在工作階段中途結束而另一個程序持有管道, 請求會繼續在已准入的身分下得到服務。
- 證明識別的是執行檔, 不是意圖。 已遭入侵的已配對用戶端程式在被撤銷之前仍然受信任。
- Windows: 持有用戶端程式管道端點者可以在其上執行伺服器。 用戶端程式以管道式 stdin 產生的子程序, 透過繼承持有該端點, 可以啟動一個證明該用戶端程式的伺服器。匿名管道是單向的, 所以它只能讀取用戶端程式自己的請求; 對用戶端程式擁有
PROCESS_DUP_HANDLE的同一使用者程序可以複製寫入端並注入請求。 - Windows: 用戶端程式被量測為 stdin 管道的建立者。 伺服器要求管道兩端指向伺服器以外的同一個程序, 對主控台或來自兩個程序的端點都失敗即關閉。
- Windows: 發行者錨點跳過撤銷檢查。
WinVerifyTrust在不做撤銷檢查的情況下執行, 這也是瀏覽器自家安裝程式驗證所採用的策略: 線上檢查會在離線時卡住啟動, 而只用快取的檢查會在從未抓取過 CRL 的機器上失敗。被撤銷的發行者憑證會保留其錨點, 直到revoke-client移除它; 以目錄簽署的映像沒有內嵌簽章, 只以雜湊作為錨點。
邊界 2: Rust MCP 伺服器 <-> 原生訊息主機 (橋接 socket, NDJSON)
唯一一道針對本機對端設防的跳點: 任何可能嘗試觸及橋接的程序。
- 所有權: 第一個綁定 socket 與鎖定檔的 MCP 伺服器執行個體是中介。之後經證明的執行個體透過同一個 socket 以中繼身分接入, 瀏覽器的原生訊息主機也一併接入, 一個共用工作階段把每個用戶端程式的工具呼叫多工到各瀏覽器連線上。
- 生命週期: 中介以用戶端程式用戶端做參考計數 (它自己的 stdio 用戶端程式加上各中繼; 瀏覽器刻意不計入), 並在最後一個離開時結束。關閉協定經 loom 模型檢查: 恰好在歸零時關閉, 終局決定之後不再有接入。
- 沒有監聽埠: 在 Unix 上, 橋接是位於 0700 使用者專屬目錄內的 0600 Unix domain socket。在 Windows 上它是一個具名管道, 其安全描述元只准許目前使用者, 由核心在開啟時強制執行。
- 對端 UID 檢查 (Unix): 在
accept時, 伺服器從核心讀取連入對端的 UID (getpeereid或SO_PEERCRED), 在認證之前就丟棄任何不是來自自身 UID 的連線。 - 相互的執行檔證明: 仍在認證之前, 每一端都為對端取得一個由核心證明的身分, 並要求它與自己正在執行的映像相符, 所以同一使用者的其他程式會在這裡被拒絕。伺服器在
accept後證明主機, 主機在connect後證明伺服器。自身身分在啟動時量測, 早於 bind、accept 或 dial。
| 作業系統 | 對端身分 | 繫結到 |
|---|---|---|
| Linux | 來自 SO_PEERCRED 的對端 pid, 接著是 /proc/<pid>/exe 的 SHA-256 | 正在執行的 inode; 解析時存在一個狹窄的 pid 重用競態 |
| macOS | 對端的核心稽核權杖 (LOCAL_PEERTOKEN), 以 SecCodeCheckValidity 驗證並按 cdhash 比對 | 正在執行的映像, 這關閉了路徑重新開啟的 TOCTOU 與 pid 重用競態; 只有 ENOPROTOOPT (較舊的系統) 才回退到以 pid 識別的 SecCode, 此時狹窄的 pid 重用競態仍然存在, 任何其他錯誤都失敗即關閉 |
| Windows | 核心為管道另一端記錄的 pid, 接著是該 pid 所執行映像檔的 SHA-256 | 檔案路徑, 重新開啟; 見下方殘餘風險 |
- HMAC 挑戰回應: 伺服器送出一個新鮮的隨機 nonce, 主機以
HMAC-SHA256(secret, nonce)回覆, 並以常數時間驗證。每次執行的秘密存放在鎖定檔中, 依僅擁有者規則保持私密, 從不經過線路傳輸; 每條連線各自的 nonce 可抵擋重放。 - 接入訊框: 交握之後, 每個對端立即送出一個宣告角色的訊框 (瀏覽器, 或中繼用戶端), 以失敗即關閉的方式讀取; 中繼的接入訊框帶上其經證明的用戶端程式身分, 供邊界 1 決定。同一標籤下的瀏覽器重新連線會取代該標籤先前的寫入者。
- 上限: 最多 16 個不同的瀏覽器標籤與 8 個用戶端程式用戶端, 最多 32 條處於交握中的連線, 一個對已准入閒置連線清除的 10 秒交握加接入讀取逾時, 以及丟棄氾濫中繼的逐中繼 GCRA 速率限制器 (突發 128, 補充 128/秒)。每條連線都是經過大小檢查的 NDJSON。
- 緊急開關: 閂鎖設定期間, 中介的監看器會在其一秒一次的滴答內切斷每條活躍的瀏覽器連線, 把進行中的呼叫排空為型別化失敗, 瀏覽器接入則在准入時被拒絕。無法讀取的信任記錄得到同樣的處置。中繼保持接入; 它們的呼叫在邊界 1 以型別化方式被拒絕。
這個跳點的殘餘風險:
- 被同一使用者的攻擊者重新執行的同一個執行檔, 與合法主機無法區分。 位元組與
cdhash完全相同, 所以雜湊與程式碼簽章都無法區分它們。在 macOS 上, 信任固定到一個確切的cdhash; 為了同時接受另一個受信任的建置而做 Team ID 或指定需求 (designated requirement) 固定, 是延後的後續工作。 - Windows 按路徑量測映像。 管道對端的 pid 解析為映像路徑, 雜湊與簽章是從該檔案讀取的, 而不是從已對映的映像, 因為使用者模式沒有它的控制代碼。執行中的執行檔可以重新命名, 所以同一使用者的程序可以在啟動與量測之間在該路徑放上另一個檔案; pid 重用競態同樣適用。
- Windows: 鎖定檔沒有設定明確的權限模式。 每次執行的秘密依賴使用者專屬執行階段目錄 (
LOCALAPPDATA, 或USERPROFILE\AppData\Local; 最後手段的暫存目錄不保證是使用者專屬的) 的預設權限。秘密只是四道閘門之一, 不是唯一一道。 - 中介聚合了影響範圍。 一個程序面向所有用戶端, 上限緩解了但沒有消除這一點; 它也是單點故障, 其當機會讓每個已接入的用戶端程式收到 EOF; 倖存者的重啟會選出新的中介。
邊界 3: Chrome <-> 原生訊息主機 (Native Messaging 訊框格式)
Chrome 依主機資訊清單產生主機, 清單中的 allowed_origins 固定了擴充功能 id, 所以只有我們的擴充功能能開啟它。擴充功能則反過來固定它配對過的主機。
- 訊框格式: 4 位元組小端序長度前綴加 JSON, 入站 64 MB 上限, 出站 1 MB 上限, 單一寫入者且逐訊框 flush, 以及
panic = "abort"加 stderr panic hook, 讓 panic 無法破壞訊框串流。主機在 stdin EOF 時關閉。 - 經模糊測試:
src/packages/core/fuzz/中的 cargo-fuzz 目標涵蓋線路解析器 (Native Messaging 訊框格式、MCP JSON-RPC、橋接與交握解碼器) 及其背後的語意驗證器 (交握 MAC 驗證器、訊框分類器、主機金鑰挑戰建構器、資訊清單所有權判定)。 - 主機處理的控制訊框由主機本身回應, 絕不轉送到 MCP 伺服器; 同樣的訊框類型若從伺服器那一側到達, 則視為注入而丟棄。架構頁面負責每種交換的順序; 帳冊列出主機終結哪些訊框:
| 家族 | 來自擴充功能 | 來自主機 |
|---|---|---|
| 主機金鑰 | enclave_challenge、enclave_revoke | enclave_proof、enclave_error (沒有金鑰時為 not_enrolled)、enclave_revoked |
| 用戶端管理 | client_list、client_revoke、client_pair | client_list_result、client_revoke_result、client_pair_result; client_pair 先開啟在場交換 |
| 緊急開關 | kill_status、kill_engage、kill_release | kill_status_result |
| WebAuthn | enroll_begin、enroll_finish、presence_begin、presence_assert、presence_confirm、browser_revoke | enroll_options、enroll_result、presence_request、presence_result、browser_revoke_result |
| 註冊 | registration_status、registration_repair | registration_status_result |
| 健康報告 | doctor_report | doctor_report_result |
| 策略與語言 | policy_get、policy_restrict、policy_set、policy_history、policy_rollback、lang_get、lang_set | policy_current、policy_restrict_result、policy_set_result、policy_history_result、policy_rollback_result、lang_current; policy_set 與放寬的 policy_rollback 先開啟在場交換 |
| 稽核 | audit_event (射後不理)、audit_read | audit_read_result |
audit_event受種類允許清單約束 (只允許擴充功能所擁有的確認與登記種類), 而且由主機自行蓋上介面標記, 所以瀏覽器那一側無法把主機側事件偽造進稽核日誌。- 稽核日誌: 安全決策 (准入、拒絕、顯示/允許/拒絕的確認、各介面的撤銷、緊急開關轉換、策略寫入、工具呼叫) 記錄到 stderr 與一個私有的、按大小上限輪替的
audit.log, 由genkan audit讀取, 也由選項頁面經唯讀的audit_read訊框讀取。格式由 CLI 頁面負責。- 擴充功能自己的環: 它的事件也會進入邊界 4 受限儲存空間之後的一個有界環 (200 筆), 由同一個面板在主機日誌旁邊顯示。
- 並非每項決策都到達主機的日誌: 兩種擴充功能本機的種類
policy_refused與policy_compromised依設計留在環中, 而一次失敗的儲存寫入會丟棄該筆記錄 (策略帳冊中的入侵標記條目)。
- 已切斷模式: 閂鎖設定期間, 或信任記錄無法讀取時, 主機只執行控制平面。它從不撥接中介, 丟棄橋接訊框, 並讓控制訊框維持運作, 使狀態、啟用與策略拉取仍可觸及。煞車 (
kill_engage) 是一個沒有閘門的訊框; 解除則開啟下方的在場交換。
主機身分。 擴充功能固定主機的 P-256 公鑰, 該金鑰由 genkan pair 在真實終端機輸入確認之後鑄造, 透過 keyring 存入作業系統憑證儲存區 (鑰匙圈、認證管理員或 Secret Service), 或以 --file-store 存入執行階段目錄中的一個檔案, 依僅擁有者規則保持私密。檔案的存在本身就是這個選擇; 儲存區失敗會被回報, 絕不改道到檔案。
- 固定: 使用者把
pair印出的指紋與擴充功能顯示的指紋比對, 這能挫敗位於兩者之間的主機。無法讀取自己金鑰的主機以失敗即關閉的方式通不過固定;revoke --all刪除金鑰, 並在儲存區確認它已消失且主機金鑰標記已變動之後推送一個由主機發起的撤銷, 所以已固定的擴充功能無需等待重新驗證即失敗即關閉。 - 金鑰的唯一工作: 向擴充功能識別這次安裝, 以及簽署策略基準。簽署不以在場為閘門; 在場是下方瀏覽器的認證器。
WebAuthn 在場交換。 擴充功能請求的每一個恢復或授予能力的動作 (解除緊急開關、登記另一個瀏覽器, 以及策略路由到此處的 page_eval 或 page_upload) 都在一個由主機鑄造並驗證的請求之後執行。
host statement = domain || 0x00 || browser label || 0x00 || action || 0x00 || nonce
challenge = sha256(statement) -> presence_request
ext navigator.credentials.get({ publicKey: { challenge, rpId: <extension id>, ... } }) <- the human gesture
host verify: rpIdHash, the user-present flag, the challenge echoed in clientDataJSON,
ECDSA P-256 over authenticatorData || sha256(clientDataJSON), against the key stored at enrollment- 誰可以作答: 每條瀏覽器連線同時只有一個未決請求, 新請求取代舊請求。解除緊急開關只接受登記在這個瀏覽器標籤下的憑證 (否則為
wrong_browser_label); 登記另一個憑證接受任一已登記憑證, 這正是讓第二個瀏覽器得以登記的條件; 未知憑證為credential_not_enrolled。 - 頁面操作的請求 (
presence_begin) 把操作與頁面的來源以page_eval on <origin>的形式綁進簽署陳述, 所以為一個頁面鑄造的觸碰無法為另一個頁面擔保。主機只為這兩個操作、且只在形如瀏覽器所序列化的來源上鑄造它, 對其他任何請求一律拒絕且不留任何未決請求, 並只接納這個瀏覽器的憑證 (沒有憑證時則接納視窗)。 - 視窗: 擴充功能的確認視窗可以作答, 回應請求的 nonce, 但只有在依作答時的登記狀態沒有任何已登記憑證滿足該請求的規則時才行 (否則為
software_confirmation_not_allowed)。瀏覽器自己的動作: 其標籤下沒有憑證。一次登記: 機器上沒有憑證, 所以第二個瀏覽器由另一個瀏覽器的觸碰登記, 絕不是一次點擊。 - 計數器: 一旦任一方有計數, 簽署計數器就必須前進, 並在信任記錄鎖下持久化, 所以重放或被複製的計數型認證器失敗即關閉。從不計數的認證器在兩側都回報零, 計數器抓不到它的複製品。
- 登記:
navigator.credentials.create只接受attestation: "none"、只接受 ES256 (P-256); 憑證金鑰取自經證明的憑證資料, 不信任任何證明鏈。機器上一個憑證都沒有時的首次登記是首次使用即信任, 在寫入時於鎖下重新檢查; 之後的每次登記都需要來自已登記憑證的斷言。 - 忘記最後一個已登記的瀏覽器會重新打開首次使用。
revoke <browser>與瀏覽器自己的「忘記」動作不需要證明, 因為它們只移除能力; 對最後一筆登記, 它們讓機器回到首次使用即信任, 於是來自任何瀏覽器的下一個enroll_begin都不經挑戰即可登記, 又回到全新機器的殘餘風險。發生時 CLI 會說明。 - 驗證失敗的斷言被拒絕並稽核, 請求被消耗, 憑證保持登記。經驗證的回答所產生的證明是一個只有在場模組才能鑄造的線性見證, 所以解除路徑不可能在未檢查在場的情況下執行。
- CLI 的下限:
pair、pair-client、unkill與policy set沒有 WebAuthn 用戶端, 所以它們的證明是在被證實為終端機的 stdin 上輸入的一句話, stdin 以管道導入時在任何提示之前即被拒絕。CLI 從不跳出瀏覽器提示。
這個跳點的殘餘風險:
- Native Messaging 資訊清單替換。 資訊清單位於使用者可寫入的目錄, 所以同一使用者的攻擊者可以把它的
path改指向一個直接以 native messaging 與真正的擴充功能對話的執行檔, 繞過邊界 2 的 socket。allowed_origins固定了哪個擴充功能可以開啟主機, 而非擴充功能接受哪個主機。- 固定關閉了什麼: 無法讀取主機金鑰的被替換主機 (另一個使用者的程序, 躲在鎖定的憑證儲存區後面的冒充者) 無法回應挑戰, 已固定的擴充功能失敗即關閉。
- 仍然開放的: 金鑰是同一使用者可讀的軟體金鑰, MV3 在每次 Service Worker 重啟時 (大約每閒置五分鐘一次) 重新產生主機, 而在場無法在每次重新連線時都要求, 所以換掉資訊清單或重新執行我們執行檔的同一使用者攻擊者, 在重新連線時無法區分。
- 它無法偽造的: 來自瀏覽器認證器的 WebAuthn 斷言。它可以偽造撤銷推送, 讓已固定的橋接失敗即關閉: 這是針對使用者自己的橋接的阻斷服務, 不授予任何能力。跳過自身驗證的被替換主機, 就是 IPC 層本來就不設防的那個同一使用者程序。
- 主機金鑰是軟體金鑰。 能讀取金鑰的同一使用者程序可以簽署一份通過固定驗證的策略基準, 而無需 CLI 所要求的終端機確認。這是逐次使用的在場移到瀏覽器認證器 (每個平台都有) 時接受的收窄; 金鑰識別這次安裝, 不再更強。
- 觸碰背後的硬體是認證器的。 使用者驗證是平台認證器的 (生物辨識、PIN 或安全金鑰), 瀏覽器測試套件的虛擬認證器證明的是協定, 不是硬體。
- 下限證明的是意圖, 不是硬體。 為沒有任何已登記憑證可作答的請求準備的視窗, 以及 CLI 的終端機, 阻止的是無聲、指令碼化與意外的授予, 而非驅動 pty 的同一使用者程序。每一次經稽核的動作都記錄授權它的路徑 (
auth=tty、auth=confirm_window、auth=webauthn:<fingerprint>), 所以由軟體證明的授予始終可以區分。 - 從認證器或作業系統密碼金鑰存放區中刪除的憑證仍保持登記。 主機看到的是登記而非認證器, 所以登記比它的憑證活得更久: 該瀏覽器會一直拒絕視窗 (
software_confirmation_not_allowed), 並且無法應答它自己的在場請求, 直到revoke <browser>忘記這次登記。 - 金鑰銷毀的紀元遞增是盡力而為。
revoke --all與pair --reset在銷毀臨界區段中清除已簽署的基準並遞增主機金鑰紀元, 但紀元寫入可能失敗: 銷毀會立即警告, 其他介面只在下一次金鑰驗證時察覺, 而在銷毀過程中簽署的首次寫入基準隨後可能落地。revoke --all是例外: 同一次寫入還會忘記登記與用戶端, 基準清除或記錄寫入失敗會作為重設失敗回報, 絕不以警告帶過。- 界限: 讓它落地仍需要一次在場證明, 而下一次配對在鑄造前的基準清除會在新金鑰存在之前移除它; 那次清除本身也是盡力而為, 失敗時警告。
- 儲存區不會回應的憑證儲存區條目, 比檔案儲存配對活得更久。
pair --reset --file-store與revoke --all在儲存區不回應時 (鎖定或不存在的 Secret Service) 照常進行, 並警告儲存區可能持有的條目被留了下來; 檔案金鑰只在檔案存在期間遮蔽它。- 順序: 儲存區變得無法觸及, 使用者配對到檔案, 之後在儲存區仍無法觸及時撤銷該檔案金鑰, 然後儲存區恢復: 舊金鑰再次生效。
- 界限: 重新固定到檔案金鑰的擴充功能, 沒有任何固定能與重新浮現的金鑰相符。仍固定到舊儲存區金鑰的擴充功能 (重新固定從未完成, 或因為主機只在乾淨的不存在時推送撤銷而撤銷推送被扣下) 會再次信任它。
- 無論哪種情況,
genkan enclave-status都會回報一把使用者以為已經消失的金鑰,policy set會用它簽署, 直到儲存區回應之後再次執行pair --reset(或revoke --all, 它還會忘記每一個瀏覽器與用戶端)。
- 註冊修復不以在場為閘門。 選項頁面的修復訊框透過與
doctor --fix和--browser相同的接縫重新註冊偵測到的瀏覽器, 或它指名的已知瀏覽器: 冪等, 只在此帳戶的範圍內, 只把瀏覽器指向這個執行檔而非其他任何東西, 與 CLI 路徑姿態相同, 而 CLI 路徑也沒有閘門。遭入侵的擴充功能獲得的, 不過是任何同一使用者程序本來就有的。- 唯一的差別: 另一個工具以我們的主機 id 寫下的資訊清單, 會被 CLI 的明確
--fix取代, 而被該訊框留下。
- 唯一的差別: 另一個工具以我們的主機 id 寫下的資訊清單, 會被 CLI 的明確
邊界 4: 擴充功能 <-> 網頁 (Chrome API / 內容指令碼 / DOM)
頁面不受信任。這是安全上最關鍵的邊界。
- 允許清單: 頁面層級的工具只在使用者核准的來源上執行; 新的來源會提示使用者並請求主機權限。頁面無法自行核准。
allowAllSites是明確的選擇啟用。 - 確認: 送出與連結點擊、
page_press、page_select、page_eval、tab_close與page_upload都在擴充功能擁有的彈出視窗 (confirm.html) 上確認, 那是一份在自己程序中執行的chrome-extension://文件, 頁面無法讀取、聚焦、覆蓋、自動點擊或自動關閉。路由器只接受來自那份確切文件的 ready 與 resolve 訊息。逾時、關閉與缺少提供者都視為拒絕。 - 提示的範圍: 只有這些高風險工具需要確認。導覽、
page_text、tab_list與經遮罩的 Cookie 或儲存空間讀取沒有逐動作提示, 所以一個已經能觸及擴充功能的驅動者可以導覽、讀取遮罩後的內容、列舉分頁, 而使用者看不到其中每一步。確認把守的是危險動作; 它不承諾什麼事都不會無聲發生。 - 綁定: 與開啟中的確認競速的導覽, 由一個在頁面中與動作原子執行的來源斷言攔截; 點擊綁定到已核准的目標描述元。
page_upload在確認之後重新檢查分頁的來源, 並把附加綁定到那時解析出的文件節點, 所以提示期間的同源導覽能通過那次檢查。 - 寬限期: 一次已核准的送出或連結點擊之後, 在每分頁鍵下的重複在
confirmGraceMs內跳過提示, 其預設值由同一節負責。page_eval被排除在外, 每次呼叫都重新確認;page_upload每次呼叫都重新確認, 並刻意不遮罩地顯示路徑, 讓使用者看到哪個檔案會離開磁碟。 - 在場路由: 策略欄位
presenceConfirm把page_eval與page_upload路由經主機的在場交換。服務在開啟任何視窗之前先向主機請求一個綁定了該操作與頁面來源的請求; 同一個視窗顯示酬載, 使用者在那裡應答該請求: 已登記認證器的觸碰, 或在這個瀏覽器沒有憑證時的軟體確認。主機驗證並稽核它。- 沒有回退: 被拒絕的請求直接拒絕, 不開視窗; 路由判定被快照進決策, 所以提示等待期間的策略變更無法改變它的路由。
- 遮罩: 頁面文字、Cookie、儲存空間與 eval 輸出在 Service Worker 中於出站處遮罩, 兩個頁面後端共用這一次。規則由安全政策負責。
- 信任狀態隔離: Chrome 預設把
storage.local暴露給內容指令碼, 所以擴充功能以setAccessLevel(TRUSTED_CONTEXTS)把storage.local與storage.session限定在擴充功能情境內。於是內容指令碼無法讀寫登記固定值、已遭入侵標記、強制執行的策略狀態 (棘輪與切換旗標) 或允許清單, 登記閘門在該限制生效之前失敗即關閉。 - 路由器同樣有閘門: 發送者不是擴充功能自身頁面的每一則執行階段訊息都被拒絕, 所以內容指令碼無法植入允許清單, 也無法讀取固定的金鑰 id 與指紋。
- 緊急開關鏡像與稽核環: 主機緊急開關狀態的 worker 專屬鏡像與擴充功能的稽核環存放在同一個受限儲存空間中。鏡像讀到已切斷、未知或格式錯誤時, 閘門拒絕; 垃圾資料絕不會對應到寬鬆的「不存在」狀態。
- 誰寫誰讀: 鏡像只從主機的緊急開關狀態訊框寫入, 絕不從執行階段訊息寫入, 且只有擴充功能自身的頁面可以讀取狀態、切換開關或讀取日誌。
- 隔離: 內容指令碼在隔離世界中執行;
page_eval使用Function建構函式, 而非內容指令碼的範疇, 其結果在遮罩前會被安全地序列化 (循環參照、DOM 節點、特殊型別)。兩個頁面後端執行同一份共用的 DOM 實作 (src/apps/extension/src/lib/dom/page-api.ts); CDP 路徑傳送的是其字串化的原始碼, 所以兩者不可能分歧。 - 唯讀憑證: 不寫入 Cookie 或儲存空間。
這個跳點的殘餘風險:
- 已核准來源上的讀取外洩。 一旦某個來源進入允許清單,
page_text、cookie_get與storage_get執行時沒有逐動作確認, 只受啟發式遮罩保護。在已核准、已登入的來源上對模型進行提示注入的頁面, 可以靜默讀取頁面內容, 以及遮罩漏掉的任何秘密 (它比對的是類權杖的形狀, 不是語意)。- 被拒絕的替代方案: 對讀取做逐動作提示, 會給幾乎每一步代理程式操作都加上一次確認, 教會使用者對守護真正危險動作的提示也一路點過去。
- 界限: 來源允許清單 (頁面必須已經位於使用者核准的地方)、作為盡力而為層的遮罩, 以及稽核日誌。
- 寬限期讓無關的同源程式碼得以執行。 一次已核准的點擊涵蓋同一來源在寬限期持續期間於同一動作種類下接下來做的任何事;
page_eval永遠不搭這趟便車。 - 視窗證明的是一次點擊, 不是在場。 它證明的是在頁面無法觸及的介面上的一次點擊, 而不是密碼學意義上的使用者在場。
- 沒有憑證的瀏覽器上的
page_eval與page_upload以軟體確認。presenceConfirm開啟時, 這種瀏覽器的應答是視窗的確認, 由主機接受並以confirm_window稽核; 關閉時, 則只由視窗確認。選擇退出該欄位仍然是一次經簽署、以在場為閘門的策略寫入。 page_upload可以附加呼叫者指定的任何本機檔案。 當該工具啟用時 (預設關閉), 它會附加呼叫提供的任何絕對路徑, 所以被誘導呼叫它的模型可以把使用者從未挑選的檔案交給頁面, 例如 SSH 金鑰。- 閘門: 選擇啟用、來源允許清單, 以及在頁面無法觸及的視窗上顯示確切路徑的逐呼叫確認。
- 開放的: 路徑沒有被限制在挑選的檔案或安全目錄內; 獲准的呼叫會附加它指定的任何檔案。已列入強化追蹤 (改用作業系統檔案選擇器, 讓模型不指定任何路徑), 之後才會建議一般用途使用檔案上傳。
- 歷史導覽以目前來源為閘門, 而非目的地。
page_back與page_forward對照分頁目前的來源檢查允許清單; 它們落到的頁面是歷史記錄所指向的任何地方。 - 信任狀態的冷啟動時窗。
setAccessLevel是非同步的, 在 worker 啟動後才套用, 所以冷啟動時有不到一毫秒的時間, 來自前一個 worker 生命週期、位於已遭入侵算繪器中的內容指令碼可能在限制生效前寫入竄改過的值。- 界限: 路由器已設閘門之後, 這是內容指令碼通往信任狀態唯一剩下的路徑。沒有使用者空間 API 能關閉它, 固定金鑰的密碼學檢查限制了、但無法抹除被植入的固定金鑰所能達成的事。
page_snapshot_precise會短暫附加偵錯工具, 所以橫幅會閃現。- CDP 模式 (
cdpMode, 主機持有的授予, 預設關閉) 把每個頁面層級工具經由chrome.debugger在頁面的主世界中路由。啟用它是一次經簽署、以在場為閘門的策略寫入, 在沒有主機金鑰的地方被拒絕。- 代價: 它繞過頁面 CSP (所以
page_eval能在嚴格 CSP 的網站上執行), 並保持持續的偵錯工具附加, 所以「Started debugging this browser」橫幅會一直顯示。允許清單、確認與遮罩不變; 更寬的攻擊面與被移除的 CSP 層是選擇啟用的代價。
- 代價: 它繞過頁面 CSP (所以
信任記錄
執行階段目錄中、緊鄰鎖定檔的 trust.json: 僅擁有者可讀, 在執行階段鎖下原子寫入, 以失敗即關閉的方式解析 (deny_unknown_fields、版本檢查、256 KiB 大小上限)。每個強制執行點都從同一份快照讀取登記、緊急開關閂鎖與用戶端允許清單。
僅擁有者指的是 Unix。 主機寫入的每一個私有檔案 (trust.json、鎖定檔、audit.log、--file-store 金鑰) 在 Unix 下都以 0600 模式建立: 原子寫入的記錄以 0600 建立後重新命名到目的地, 就地開啟的檔案在開啟的控制代碼上重新收緊, 所以預先放置的較寬鬆檔案無法保留其權限位元。
在 Windows 上沒有模式分支: 檔案繼承使用者專屬執行階段目錄的 ACL, 即邊界 2 點名的殘餘風險。
| 欄位 | 意義 | 寫入者 |
|---|---|---|
epoch | 每次寫入都遞增: 記錄的全域排序, 範圍標記在其範圍變更時複製的值。從不是權威: 沒有任何決策或推送以它為鍵 (中介讀取它只為了去重一行日誌), 每個決策都從記錄重新讀取 | 每個寫入者 |
kill_epoch | 最後一次緊急開關閂鎖寫入 (啟用或解除, 不論閂鎖是否改變) 時的 epoch (0 = 從未), 與閂鎖在同一次原子寫入中蓋章; 它在兩次輪詢之間變動時, 原生訊息主機的監看器推送緊急開關狀態 | killed 的寫入者 |
host_key_epoch、policy_epoch、lang_epoch | 最後一次主機金鑰銷毀 (撤銷或重設, 從不是鑄造)、主機持有策略寫入或語言變更時的 epoch (0 = 從未)。監看器在兩次輪詢之間比較各自的值, 只推送對應的訊框 (撤銷、policy_current、lang_current); 撤銷推送還需要儲存區確認金鑰已消失, 所以亂寫的標記無法偽造一次推送。用戶端或登記寫入不蓋任何章, 也不觸發推送 | 該範圍變更的寫入者, 在其自身儲存之後的第二次寫入中 |
killed | 全域緊急開關閂鎖 | CLI 上的 kill 與 unkill; 擴充功能的啟用與解除訊框 |
clients | null 表示從未配對 (不強制准入); 一份清單, 即使是空的, 表示已登記且已鎖定 | CLI 上的 pair-client、revoke-client 與 revoke --all (後者清空已配對的清單, 而讓 null 保持原樣); 擴充功能經主機中介的用戶端撤銷訊框 (其用戶端清單訊框只讀) |
enrollments | 每個瀏覽器在其標籤下登記的 WebAuthn 憑證, 連同其簽署計數器 | 邊界 3 的登記交換; 在場驗證器, 在每一次接受的斷言上推進憑證的計數器; CLI 上的 revoke <browser> 與 revoke --all, 以及擴充功能針對已連線瀏覽器自身標籤的 browser_revoke 訊框, 都不經證明即忘記, 因為那只移除能力 |
- 三個章是盡力而為。
host_key_epoch、policy_epoch與lang_epoch在同一次持鎖期間、在該範圍自身儲存之後的第二次寫入中落地。寫入失敗時變更仍然成立, 主機警告, 丟失的只是監看器的推送。- 誰仍能得知: 發出請求的瀏覽器會收到回覆訊框; 在別處做出的變更會在已連線擴充功能的下一次連線時到達, 那時策略與語言訊框會無條件推送。銷毀的情況是邊界 3 的殘餘風險。
- 重新配對即取代:
pair-client取代同名條目, 這是雜湊錨點在重新簽署後的重新配對路徑。revoke-client即使清空也保留清單。 - 解除配對時兩半都刪: 從任一側解除配對都會刪除憑證的兩半; 擴充功能側的撤銷還會要求主機刪除其金鑰, 持久地重送直到被確認。
- 撤銷延遲: socket 那一段是即時的; 擴充功能在下一次 Service Worker 喚醒時反映主機金鑰撤銷。
- 強制執行: 0700 執行階段目錄上的檔案權限、原子寫入紀律, 以及在每次准入與工作階段內請求時對記錄的重新讀取。
記錄的殘餘風險:
- 同一使用者的寫入者擁有這份記錄。 能執行我們 CLI 的同一使用者程序可以把自己配對進來、植入一筆登記或翻轉閂鎖, 正如它可以刪除這份記錄一樣。刪除
trust.json會回到以 ERROR 記錄的引導狀態: 這是不可消減的同一使用者殘餘風險, 因為沒有任何使用者空間標記能在一個可以刪除我們能寫的任何檔案的寫入者面前存活。 - 竄改紀元或某個範圍標記可以強制一次多餘的推送, 或讓每次讀取都失敗即關閉, 但無法讓配對清單、主機金鑰或登記所不認可的任何人獲得准入。
- 啟用有一個次秒級的時窗 (一個監看器滴答) 留給進行中的工作, 隨後被切斷的 socket 將其排空。沒有任何東西會自行清除閂鎖: 沒有逾時、重啟或重新連線; 被拒絕的斷言絕不回退到視窗, 兩種轉換在記錄無法讀取時都拒絕, 因為從未知狀態解除會失敗即開放。
主機持有策略的殘餘風險帳冊
安全策略由主機持有: 一份經簽署的基準、一層未簽署的限制覆蓋層、擴充功能側的值棘輪, 以及每連線的分派屏障。架構頁面負責機制; 本帳冊負責殘餘風險, 每一項都是刻意接受的。
請把這些條目放在一起讀; 它們會組合。第一條的界限 (只接受儲存修訂版本或更高的正版基準) 錨定在儲存的棘輪記錄上, 而冷啟動條目使該記錄在其時窗內可被刪除: 錨點一旦被刪除, 該金鑰曾簽署過的最寬鬆正版基準就會以首次策略的身分套用, 不論其修訂版本。
- 一次失敗的入侵標記持久化加上一次 worker 重啟, 會讓屏障對被擷取的正版策略重新開啟。 當一次推送對固定金鑰的簽章驗證失敗時, 擴充功能會立即閂住記憶體中的入侵旗標, 然後寫入持久的標記。如果該寫入失敗, 且 worker 隨後重啟, 新的 worker 會在未閂住的狀態下啟動, 所以重放的逐位元組相同的推送能通過驗證, 以冪等重放的方式推進棘輪, 並重新開啟分派屏障。
- 前提: 一次持久儲存寫入失敗、一次 worker 重啟, 以及一個被擷取的正版訊框。
- 界限: 屏障只會對固定金鑰在儲存修訂版本或更高簽署過的某個正版策略重新開啟。攻擊者只能在其擷取到的文件中挑選, 沒有主機金鑰就無法偽造越過棘輪與簽署 touched-set 規則的新放寬, 而能讀取軟體金鑰的同一使用者程序擁有該金鑰 (邊界 3)。
- 重新證明只在使用者選擇加入定期重新驗證的情況下縮小它 (
hostReverifyMs; 預設為 0 時沒有任何重新連線會被挑戰)。在單一 worker 生命週期內, 記憶體中的閂鎖無論如何都成立。 - 證據: 失敗的持久化會被記錄, 從不吞掉, 但沒有任何持久證據能留存: 稽核事件寫入的是剛剛失敗的儲存空間, 而入侵種類是擴充功能本機的, 從不轉送到主機的日誌。
- 接受, 因為記憶體中的閂鎖無法在重啟後存活; 要完全關閉它需要「持久寫入成功才繼續」或開機時重新證明, 而不是更寬的閂鎖。
- 同金鑰復原路徑中的一個微任務時窗。 還原被取代的已儲存策略記錄的寫入復原, 會在其還原寫入之前立即重新檢查固定金鑰重設紀元, 但
chrome.storage不是交易式的, 所以在那次檢查與寫入之間完成的同金鑰重新配對, 可能看到復原還原了重設剛剛移除的記錄。- 構造上是單向的: 紀元在讀取先前記錄之前就被擷取, 所以另一側的誤觸會把還原變成移除 (失敗即關閉), 而剩下的那個方向只能復活一筆無作用的記錄, 範圍檢查會讓它在任何其他固定金鑰下都無法進入強制執行。接受: 沒有交易式儲存就無法修正。
- 單次推送的不一致。 兩個強制執行點 (主機分派、擴充功能閘門) 可能在策略變更前後短暫不一致; 時窗是一次推送, 而擴充功能在其邊界上是權威。
- 與錯誤推送的驗證競速的請求。 策略訊框在請求閘門之前路由, 但不與請求佇列排序, 所以在錯誤簽章的推送抵達與其驗證失敗之間 (一次訊框跳躍、兩次解析、一次 base64 解碼、一次固定金鑰讀取、一次 ECDSA 驗證), 已經通過閘門的請求仍可能在仍然開啟的屏障下分派。
- 界限: 執行期間的入侵閂鎖在驗證失敗的那一刻同步設定, 且每個請求會讀取它兩次 (在閘門處, 以及在分派自己的策略讀取時), 所以請求必須在失敗落地之前通過兩次讀取; 漏過去的請求是在儲存的有效策略下執行的, 那是經棘輪固定的下限, 從不比使用者上次看到套用的更寬鬆。
- 被拒絕的在場請求會拒絕該操作, 不回退到視窗。 在場路由的探測就是主機對
presence_begin的回答: 一次拒絕 (擴充功能自己的: 已有交換在進行或主機已消失; 或主機的: 帶有 名冊 中的一個代碼) 拒絕該操作; 一個提示沒有憑證的請求則是主機在說視窗可以作答, 即有文件記載的退化介面。- 今天: 提供者選擇是同步的, 而不存在或已遭入侵的固定金鑰會在任何確認之前於登記閘門處拒絕頁面操作。
- 為何不回退: 回退會讓任何探測失敗都把需要在場 (
presenceConfirm) 的核准降級為普通的視窗點擊, 即在場階梯的不降級規則被反轉。代價是異常路徑上的可用性, 從來不是能力。
- 敵對的未固定主機可以用放寬提示占據確認 FIFO。 在沒有固定金鑰的機器上, 放寬會被保留等待使用者的視窗核准。對待處理推送的逐位元組相同重放 (相同基準位元組、相同覆蓋層、相同連線) 會被合併到它的提示上; 不同的候選各自序列化一個提示, 所以敵對主機可以讓 FIFO 一直忙碌, 只要使用者一直不回答。
- FIFO 是全域的, 跨所有確認類型, 所以這種占據也會餓死排在它後面的
page_eval、page_upload與點擊確認。 - 兩點釐清: 合併以 JSON 字串化的覆蓋層為鍵, 所以鍵順序重排或微幅變化的限制可以擊敗它 (代價是多一次提示, 從不壓制不同的推送); 而偽造的未簽署限制會靜默套用, 完全沒有提示, 這是策略設計讓步的偽造限制阻斷服務, 因為它只會移除能力。
- 界限: 一次一個提示, 每次拒絕都有稽核, 沒有明確核准就不會放寬任何東西, 且已固定金鑰的機器不受影響 (那裡不存在核准視窗)。
- 一個外溢: 語言通道共用同一條訊框通道, 所以在未固定金鑰的機器上, 排隊中的 120 秒核准提示可能延遲語言選擇的回應。無害: 本機的
uiLanguage寫入已經落地, UI 在中繼之前就已經切換; 只有診斷用的sent旗標被延遲, 而語言選擇在未固定金鑰的機器上本來就回報false。 - 接受, 因為未固定金鑰的核准視窗本質上可被唯一獲准請求它的對端占據。
- FIFO 是全域的, 跨所有確認類型, 所以這種占據也會餓死排在它後面的
- 明確選擇「en」會在每次重新連線時重新提供首次配對的語言採用。 主機的語言儲存把不存在的記錄對應到預設值 (
"en", seq 0), 而攜帶已儲存值的lang_set是刻意的無操作, 不會遞增任何東西, 所以對從未設定過的儲存, 一次明確的"en"會讓 seq 停在 0。- 效果: 主機無法區分「從未設定」與「明確設定為預設值」, 這樣的擴充功能在每次重新連線時都重送它那一個採用訊框。
- 界限: 每次連線一個訊框 (每連線的採用閂鎖在一條連線內就結束它), 該訊框在主機側是無操作, 而且這條通道純屬外觀: 語言是瀏覽器持有的顯示狀態, 從不是策略。按現狀接受: 把「明確設定為預設值」折入 seq 遞增, 會波及 seq 存在目的所在的回音抑制語意, 為了外觀上的收益冒真實的風險。
- 損毀的入侵標記被讀成未遭入侵。 持久標記的讀取器在遇到無法解析的記錄時回傳 null, 所以損毀的標記在繼承它的每一個讀取點都被讀成「沒有證據」, 包括登記閘門、在場能力探測, 以及配對通知抑制。
- 界限: 寫入路徑被限定在受信任情境的儲存空間, 所以要製造這種損毀需要擴充功能情境或冷啟動時窗, 而不是網頁或穩定狀態下的內容指令碼。固定金鑰記錄自身的自我檢查 (其金鑰 id 必須與儲存的公鑰相符) 是內部一致性, 不是真實性, 但損毀的標記無法偽造或更改固定金鑰。
- 冷啟動時窗也擋在策略狀態前面。 邊界 4 的次毫秒
setAccessLevel時窗涵蓋策略棘輪、切換旗標、持久入侵標記, 以及先前固定金鑰記錄。與登記閘門的一個誠實差異: 策略訊框在閘門之前路由, 而策略路徑本身不等待限制生效, 所以策略狀態依賴的是限制被及早套用, 而不是在使用時重新檢查。 - 「已固定金鑰的擴充功能拒絕未簽署的基準」只是已固定情況下的界限。 在擴充功能沒有固定金鑰的機器上, 核准視窗通道會在一次新鮮、明確的使用者核准之後接受未簽署的文件。沒有任何主機路徑會寫入未簽署的基準, 所以視窗通道自身的讓步 (驅動擴充功能頁面的軟體可以回答其提示) 就是全部的暴露面。
- 與
cdpMode撤銷競速的決策可能產生短暫的偵錯工具附加。 新 CDP 工作階段的附加在附加後策略重新檢查讀取授予之前就已完成, 所以快照到cdpMode: true的進行中決策, 在撤銷落地之後仍可能附加。- 界限: 該工作階段上不會送出任何 CDP 命令; 重新檢查會拆除它並讓操作失敗, 附帶說明是工作階段層會吞掉卸離錯誤, 所以拆除是盡力而為。在附加前重新檢查曾破壞固定的附加協定順序。
- 固定金鑰撤銷會讓現有的 CDP 附加留在原處。 由策略驅動的拆除在儲存寫入時觸發, 而撤銷路徑刻意保留已儲存的策略記錄 (同金鑰的反重放錨點), 所以沒有寫入會觸發, 現有的附加連同橫幅會一直存活到分頁關閉。
- 界限: 切換之後, 分派屏障會立即停止所有新工作; 切換之前的擴充功能沒有屏障, 改為強制執行拒絕基準。附加只能滯留, 無法行動。
不得倒退的不變量
- 兩種執行檔模式下的 stdout 都只有協定位元組; 診斷輸出走 stderr。
- 橋接絕不服務任何未通過以下檢查的連線: 同一使用者檢查 (Unix 上的對端 UID, Windows 上的管道描述元)、執行檔證明、HMAC 交握, 或必要的宣告角色接入訊框。
- 一旦受信任用戶端允許清單存在, 任何用戶端程式除非其經證明的身分與某個條目相符, 否則不予服務; 無法量測的身分與無法讀取的允許清單都失敗即關閉, 而用戶端自稱的名稱絕不是授權的鍵。
- 主機資訊清單的
allowed_origins永遠恰好固定我們的擴充功能 id。這固定了擴充功能到主機的方向; 主機到擴充功能的跳點由邊界 3 的固定主機金鑰把守。 - 沒有任何頁面層級工具會在未列入允許清單的來源上執行, 除非設定了
allowAllSites。 - 沒有任何工具會寫入 Cookie 或網頁儲存空間; 刻意不提供
cookie_set或storage_set。 - 確認閘門預設開啟, 放寬任何一道都是一次經簽署、以在場為閘門的策略寫入。
- 緊急開關啟用期間, 或其記錄無法讀取時, 不服務任何工具呼叫, 也不允許任何瀏覽器連線存在; 開關只能由受信任介面的明確、經在場證明的解除動作清除, 絕不會自動清除。
- 稽核日誌從不作為決策的閘門: 記錄採「先決定後記錄」, 寫入失敗時以可見的方式丟棄該筆記錄 (
dropped計數器), 而不會讓操作在任一方向上失敗。
變更以上任何一條都是審查標準下的安全相關變更。