---
title: C18 api → License Center
eyebrow: Guidant AI 資安檢視總報告 · 連線清單
h1: C18 api → 授權簽發站（License Center，落地版啟用時）
lede: 落地版產品向原廠的授權簽發站換取、展延授權檔走的那條線。它有兩個方向的不信任：**我們不信對面**（簽發站回來的授權檔照樣本地驗章），**對面也不信我們**（開通序號本身就是密碼）。流的是開通序號、機器指紋、簽好的授權檔，以及後台請照用的 API token。這一頁回答：這條線**怎麼連**、**哪些功能走它**、它帶來**什麼威脅、駭客怎麼打**、我們**要怎麼防、目前做到哪**。
chips:
  - { text: "落地版啟用時", kind: plain }
  - { text: "序號即密碼", kind: warn }
  - { text: "威脅 3 種", kind: accent }
  - { text: "掃描命中 3 條", kind: plain }
---

> **這一頁怎麼來的**：[DFD Level 0](../DFD/dfd-level0.html#c18) 把產品運作時的連線編成 C01～C25；[STRIDE 六頁](../STRIDE/S-spoofing.html)每一條問題都標了發生在哪幾條線。這裡以**連線**為單位整理。3 條命中裡 M18-3 的洞在**簽發站那端**（獨立系統、獨立 repo），本頁列為實例是因為攻擊者打的就是這條線的另一頭；M18-1、M18-7 的洞在產品這端的驗章與停權邏輯。個別問題修了沒不在這頁講。

## 一、連線圖

```{.mermaid cap="C18 — api 到授權簽發站。兩種入口：客戶在線上開通頁填序號（api 代打簽發站換照）；原廠後台用 API token 請照／展延。不論哪條路，拿回的授權檔都走同一支本地驗章（Ed25519，按 kid 查內建公鑰表），host 版再核對機器指紋。紅色是三個被打過的點：序號曾只有 8 碼、公鑰表三個環境並列、停權曾記在可被換掉的授權檔上。"}
%%{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](C02-frontdoor-to-api.html)，驗章邏輯與 ① ② 同一支。

## 四、這條線會帶來什麼威脅、駭客怎麼打

掃描在這條線上抓到 3 條問題（[M18-1](../STRIDE/E-elevation-of-privilege.html#m18-1)、[M18-3](../STRIDE/S-spoofing.html#m18-3)、[M18-7](../STRIDE/S-spoofing.html#m18-7)），歸納起來是**三種攻擊手法**。受損的都是**原廠的收入與商務控制**，不是客戶資料。

| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🟠 高 | **大量猜序號，領走別人付費的授權** | ① 不需要任何帳號，直接打簽發站的開通 API（這條線的另一頭對網路開放）；② 用程式大量送隨機序號配上自己機器的指紋；③ 簽發站的鎖定按「這個序號錯幾次」計，每次換一個序號就永遠不鎖（修前序號只有 8 碼、約 43 億組）；④ 撞中一張別人尚未兌換的序號，那張授權就綁到攻擊者的機器 | 領走別人付費的授權 | [S 冒充身分](../STRIDE/S-spoofing.html) | [M18-3 開通序號只有 8 個字，猜得到別人的授權](../STRIDE/S-spoofing.html#m18-3) 🟠 |
| T2 | 🟠 高 | **被停權後自己換一張照復權** | ① 平台把某客戶停權（欠費或違約），產品轉唯讀；② 該客戶任一登入帳號把手上的舊授權檔重新上傳、或走線上開通換一張；③ 系統建一筆新的授權紀錄取代現行照，停權欄位記在舊照上、新照是空的；④ 唯讀當場解除，不需要平台同意 | 被停權的客戶自行改寫授權狀態；平台的最後商務手段形同無效 | [E 權限提升](../STRIDE/E-elevation-of-privilege.html)、[T 竄改資料](../STRIDE/T-tampering.html) | [M18-1 被停權的客戶自己上傳舊授權檔就能復權](../STRIDE/E-elevation-of-privilege.html#m18-1) 🟠 |
| T3 | 🟡 中 | **用開發環境的私鑰簽出正式環境認的照** | ① 取得開發環境簽發站的私鑰（例如某台開發機外流）；② 自己簽一張全功能、永不到期的授權檔；③ 上傳到正式環境；④ 產品照 `kid` 在內建公鑰表找到開發環境那把，驗章通過 | 改寫自己的授權內容（功能、到期日、客戶數） | [S 冒充身分](../STRIDE/S-spoofing.html)、[T 竄改資料](../STRIDE/T-tampering.html) | [M18-7 三個環境的公鑰並列，開發環境簽的照正式也認](../STRIDE/S-spoofing.html#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 同帳號兩家並用是另一面）。
- **離線上傳與線上開通共用驗章**：D4 的驗章是兩條路共用的單一支，好處是守一處；壞處是那一支若有洞（如 M18-2 修前的解壓炸彈），兩條路都開。

---

*依據：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 頁記載引用。*
