Guidant AI 資安檢視總報告 · 連線清單
落地版產品向原廠的授權簽發站換取、展延授權檔走的那條線。它有兩個方向的不信任:我們不信對面(簽發站回來的授權檔照樣本地驗章),對面也不信我們(開通序號本身就是密碼)。流的是開通序號、機器指紋、簽好的授權檔,以及後台請照用的 API token。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。
這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這裡以連線為單位整理。3 條命中裡 M18-3 的洞在簽發站那端(獨立系統、獨立 repo),本頁列為實例是因為攻擊者打的就是這條線的另一頭;M18-1、M18-7 的洞在產品這端的驗章與停權邏輯。個別問題修了沒不在這頁講。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB'}}}%%
flowchart LR
CUST(["👤 客戶任一登入帳號<br/>(開通頁)"])
OPS(["👤 原廠後台管理員"])
ANY(["🕵 網路上任何人"])
subgraph MAIN["🏠 主系統 api"]
direction TB
ACT["線上開通<br/>序號+機器指紋"]
ADMIN["後台請照/展延<br/>X-API-Token"]
VER["本地驗章<br/>Ed25519・查 kid・長度與解壓上限<br/>host 版核對指紋"]
PK[("內建公鑰表<br/>DEV・STG・POC 三把並列")]
SUS[("停權表<br/>tenant_license_suspensions<br/>記在客戶身上,不在授權檔上")]
ACT --> VER
ADMIN --> VER
PK --> VER
VER --> SUS
end
LC(["🏛 License Center<br/>LICENSE_ACTIVATION_SERVER_URL"])
CUST -- "C02 · 填序號" --> ACT
OPS -- "C02 · 請照/展延" --> ADMIN
ACT == "C18 · POST /api/activation/activate" ==> LC
ADMIN == "C18 · POST /api/internal/issue・extend<br/>X-API-Token" ==> LC
LC -. "簽好的授權檔" .-> VER
ANY -. "直接打開通 API 猜序號" .-> LC
classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
classDef open fill:#FDECEC,stroke:#C0392B
class CUST,OPS,ANY,LC ext
class PK open
| 項目 | 現況 |
|---|---|
| 誰 → 誰 | guidant-api → License Center(獨立系統、獨立 repo、獨立 DB,與主產品只靠「簽好的授權檔」交互,零程式依賴)。位址 LICENSE_ACTIVATION_SERVER_URL(環境變數,預設 http://127.0.0.1:5062) |
| 協定/埠 | HTTP(S),httpx,逾時 10 秒(兩條路各自 _ACTIVATION_SERVER_TIMEOUT/_LC_REQUEST_TIMEOUT)。scheme 由環境變數決定,預設是 http;憑證驗證是 httpx 預設開 |
| 兩條路 | ① 客戶線上開通:POST /license/activation/online(登入即可)→ api 帶開通序號+本機機器指紋打簽發站 /api/activation/activate → 收回簽好的授權檔;② 原廠後台請照/展延:POST /license/tenants/{uid}/lc-issue/lc-extend(平台管理員)→ api 帶 X-API-Token(LICENSE_CENTER_API_TOKEN)打 /api/internal/issue/extend/plans |
| 收回來的東西怎麼驗 | 不因來源是簽發站就信:兩條路收到的授權檔都走 activate_license()/upload_license() 完整驗章——v2 信封 {kid, payload, signature},Ed25519,按 kid 查內建 PUBLIC_KEYS;解碼前先擋長度(32 MiB)、解壓有界(16 MiB);host 版再比對機器指紋;時鐘回撥檢查(clock_watermark) |
| 停權怎麼記 | 新表 config.tenant_license_suspensions,一列=該客戶目前被停權;與授權檔脫鉤(M18-1 修後)。客戶換照不會解除停權 |
| 公鑰表 | jedi_license_runtime/common/public_keys.py 的 PUBLIC_KEYS:DEV、STG、POC 三把並列、都編進產品;任一環境簽的照其他環境都認(M18-7,裁定不修,交付首位客戶前必做) |
| 何時存在 | 落地版啟用/展延時。授權檔本身也可離線上傳(/license/activation/upload、/license/upload),那條不走這線 |
依據:套件 jedi_license_runtime/app/service/license_verification_service.py(activate_license_online()、_verify_signed_license())、tenant_license_admin_service.py(_call_lc()、_call_lc_get())、common/engine.py(verify_payload()、_unpack_payload() 兩道上限)、common/public_keys.py、api/routing.py(路由與守門等級);主專案 config/config.py:320-327(LICENSE_ACTIVATION_SERVER_URL、LICENSE_CENTER_API_TOKEN)。
DFD 對照:DFD 寫「HTTPS;簽章授權檔」。實查 scheme 由環境變數決定、預設值是 http://127.0.0.1:5062(本機開發用),正式部署要靠部署文件把它設成 https——程式沒有強制。已回寫本卡。另補 DFD 沒寫的:後台請照那條路帶 X-API-Token,是第二種憑證。
| 群 | 功能 | 對威脅的意義 |
|---|---|---|
| ① 客戶線上開通 | 開通頁填序號 → 換回綁本機的授權檔 | 觸發者是客戶任一登入帳號;序號是唯一憑證——簽發站那端「持有序號=持有那張授權」 |
| ② 原廠後台請照/展延 | 原廠管理員在產品後台替某客戶請一張新照、或展延現行照 | 帶 X-API-Token 出門;token 在環境變數。展延=換一張新照取代舊照(replace 制) |
| ③ 方案清單 | 後台請照表單的下拉選單透傳簽發站的方案清單 | 唯讀,無落地 |
不在這條線上但共用驗章:離線上傳授權檔(客戶自己拿檔案上傳)走 C02,驗章邏輯與 ① ② 同一支。
掃描在這條線上抓到 3 條問題(M18-1、M18-3、M18-7),歸納起來是三種攻擊手法。受損的都是原廠的收入與商務控制,不是客戶資料。
| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🟠 高 | 大量猜序號,領走別人付費的授權 | ① 不需要任何帳號,直接打簽發站的開通 API(這條線的另一頭對網路開放);② 用程式大量送隨機序號配上自己機器的指紋;③ 簽發站的鎖定按「這個序號錯幾次」計,每次換一個序號就永遠不鎖(修前序號只有 8 碼、約 43 億組);④ 撞中一張別人尚未兌換的序號,那張授權就綁到攻擊者的機器 | 領走別人付費的授權 | S 冒充身分 | M18-3 開通序號只有 8 個字,猜得到別人的授權 🟠 |
| T2 | 🟠 高 | 被停權後自己換一張照復權 | ① 平台把某客戶停權(欠費或違約),產品轉唯讀;② 該客戶任一登入帳號把手上的舊授權檔重新上傳、或走線上開通換一張;③ 系統建一筆新的授權紀錄取代現行照,停權欄位記在舊照上、新照是空的;④ 唯讀當場解除,不需要平台同意 | 被停權的客戶自行改寫授權狀態;平台的最後商務手段形同無效 | E 權限提升、T 竄改資料 | M18-1 被停權的客戶自己上傳舊授權檔就能復權 🟠 |
| T3 | 🟡 中 | 用開發環境的私鑰簽出正式環境認的照 | ① 取得開發環境簽發站的私鑰(例如某台開發機外流);② 自己簽一張全功能、永不到期的授權檔;③ 上傳到正式環境;④ 產品照 kid 在內建公鑰表找到開發環境那把,驗章通過 |
改寫自己的授權內容(功能、到期日、客戶數) | S 冒充身分、T 竄改資料 | M18-7 三個環境的公鑰並列,開發環境簽的照正式也認 🟡 |
三種手法的共同點:這條線的信任全靠密碼學——序號是密碼(T1)、簽章是信任根(T3)——而密碼學的強度取決於「夠不夠長」與「哪些鑰匙算數」。T2 不一樣:它是「商務狀態記在客戶能換掉的東西上」,與加密無關,是資料模型的問題。
對應三種威脅與這條線的本質,防線分六條。D1~D3 直接對應掃描抓到的手法;標 ◇ 的(D4~D6)是依這條線的特性補的標準防線,掃描範圍沒涵蓋、沒有實例。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。
| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 | 序號要夠長、有效期、失敗鎖定按來源計。約 128 位元隨機值;未兌換的序號有效期;鎖定按來源 IP 存資料庫,另有全站失敗預算擋分散式猜測 | T1 | ✅ 簽發站 CM-1583:secrets.token_urlsafe(16)、30 天效期、按 IP 鎖定+全站預算、序號與指紋欄位加長度上限、紀錄不帶明文前綴、補發作廢舊序號。驗證:連猜 6 個不同序號第 6 次 429。這一條在簽發站 repo,不在產品 repo |
| D2 | 停權記在客戶身上,不記在授權檔上。換照不影響停權;解除停權是刪那一列 | T2 | ✅ 新表 tenant_license_suspensions,force_lock_tenant()/unlock_tenant() 寫新表,LicenseGuard.snapshot_for() 讀新表。⚠️ 新表一出生帶著寫反的 RLS,子單位可刪母公司的停權紀錄——另案 M18-8 |
| D3 | 出貨版只認正式簽發站的公鑰。開發、測試環境的公鑰不進客戶拿到的 image | T3 | 🚫 裁定不修(已記錄):三把公鑰對應三台內部簽發站、私鑰都在內部,目前沒有正式簽發站也沒有外部客戶。失效條件:交付首位客戶前,出貨版改成只認正式公鑰。打包流程的正式公鑰守門路徑已修正(CM-1581)但目前守的是三把並列的表 |
| D4 ◇ | 收回來的授權檔不因來源是簽發站就信,一律本地驗章+有界解析 | 簽發站被假扮、或簽發站本身被攻破後回假照 | ✅ activate_license_online() 明寫「收到照後仍走完整驗章+指紋核對,BE 是唯一信任邊界」;_unpack_payload() 解碼前擋 32 MiB、解壓有界 16 MiB(M18-2,CM-1572);時鐘回撥檢查 |
| D5 ◇ | 這條線只走 https,位址不可被改成攻擊者的簽發站 | 假扮簽發站拿 X-API-Token、或回假照(D4 擋得住假照,擋不住 token 外流) |
⚠️ LICENSE_ACTIVATION_SERVER_URL 預設 http://127.0.0.1:5062,程式不強制 https;正式部署靠部署文件設對。token 外流的損害:能用原廠身分向簽發站請照(簽發站那端的 /api/internal/* 守門是 token) |
| D6 ◇ | X-API-Token 的保管與輪替。它是原廠對簽發站的身分,與 DB 密碼同級 |
後台請照那條路被冒用 | ⚠️ 環境變數(.env);沒有輪替機制、沒有到期;簽發站端有沒有按 token 記來源與限速,屬 LC repo 範圍,本頁沒查 |
最便宜的一步:D5——config.py 讀 LICENSE_ACTIVATION_SERVER_URL 時,非 DEV 環境且 scheme 不是 https 就拒絕啟動(與 SANDBOX_MODE=inprocess 只准 DEV 同一個模式)。D3 是最重要的一步,但已裁定綁交付首位客戶的時點。
/api/internal/* 的守門(D6):產品這端帶 token 出門,簽發站那端怎麼驗、有沒有限速與來源記錄,在 LC repo,本輪掃描範圍外。LICENSE_ACTIVATION_SERVER_URL 的 http 預設(D5):本頁開檔新發現,不在 168 條裡;正式環境若忘了改,序號、指紋、token 全明文。compute_machine_fingerprint() 算什麼、能不能在另一台機器上複製出同一個值,決定「一張照只能一台機器用」守不守得住(M18-9 同帳號兩家並用是另一面)。依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本)、套件 jedi-license-runtime(app/service/license_verification_service.py、tenant_license_admin_service.py、common/engine.py、common/public_keys.py、api/routing.py)、主專案 config/config.py、DFD Level 0。M18-3 的修正在 License Center repo(CM-1583),本頁依 STRIDE 頁記載引用。