---
title: "License 是怎麼運作的 — 原理說明 (FR-062)"
brand: "Guidant AI · **FR-062** License 控管機制"
eyebrow: "FR-062 · License 運作原理 · 2026-09-06"
h1: "License 是怎麼運作的"
lede: "從「一張照怎麼被簽出來」到「客戶機器上怎麼被驗開」，把整條鏈的**原理**講清楚。這份文件回答的是 **Why**——為什麼是簽章不是加密、canonical JSON 為什麼非有不可、「先解壓再驗簽」的順序藏著什麼含意。規格條列請看 [`design.html#sign-verify`](./design.html)（§4.2），本文是它的展開版。"
chips: [
  {text: "數位簽章，不是加密", kind: accent},
  {text: "Ed25519 · 私鑰在簽發站 · 公鑰編譯進產品", kind: ok},
  {text: "含 CM-1572 壓縮炸彈的成因說明", kind: warn}
]
footer: "FR-062 · License 運作原理 — 2026-09-06 · 事實來源：license_center crypto/engine.py、jedi-license-runtime common/engine.py"
---

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 原因 |
|------|------|------|
| 2026-09-06 | 新建。從 design.md §4.2（7 個 bullet 的規格條列）展開為原理說明，補齊簽章 vs 加密的觀念澄清、canonical JSON 的存在理由、驗證順序與 CM-1572 的因果關係、機器指紋、兩種開通路徑對照、機制邊界的誠實說明 | 決策者提問「License 到底怎麼產生、怎麼加密驗證」，發現既有文件只有規格沒有原理，答不了 |

---

## 1. 一句話總覽 {#overview nav="總覽"}

公司在內部簽發站用**私鑰**對一段授權內容（誰的照、什麼方案、幾個人、到什麼時候）簽名，
把「內容 + 簽名」包成一個檔交給客戶；客戶的系統裡編譯著對應的**公鑰**，收到檔就用公鑰驗一次簽名——
**驗得過，代表這份內容確實出自我們、而且一個字都沒被改過**。

```
公司這邊：內容 ──私鑰簽名──▶ 照檔（內容＋簽名）──交付──▶ 客戶
客戶那邊：照檔 ──公鑰驗簽──▶ 通過 → 開通；不過 → 拒絕
```

### 🔴 最重要的觀念：我們用的是數位簽章，不是加密 {#not-encryption nav="不是加密"}

這個區別是理解整套設計的鑰匙，所以放在最前面。

::: {.callout .crit}
**照的內容，客戶看得到是沒關係的。**

授權內容（租戶代碼、方案、人數上限、到期日）本來就是客戶花錢買的東西——他當然知道自己買了什麼。
我們要防的**不是「被看見」，而是「被改掉」**：把到期日從 2026 改成 2099、把 10 人改成 1000 人、
把沒買的模組加進清單。

**加密**解決的是「不讓人看」，**簽章**解決的是「改了就驗不過」。我們要的是後者。
:::

程式碼裡對這件事講得很直接（`license_center/src/license_center/core/crypto/engine.py` 檔頭）：

> 編碼打包防客戶隨手翻閱，不是加密（Host 版客戶持有整包程式碼，解碼邏輯終究可逆向，此設計本就不承諾密碼學保密）

這句話點出的是**落地版的現實**：客戶機房裡跑的是整包我們給的程式碼，解碼邏輯就在裡面，
再怎麼混淆終究拆得開。所以我們從一開始就沒有把「保密」當成承諾——
照裡有一層 zlib 壓縮 + base64，效果是「隨手用文字編輯器打開看不到明文」，
擋的是好奇心不是攻擊者。**真正的保證只有一條：竄改必然被抓到。**

---

## 2. 簽章的原理：一把私鑰、一把公鑰 {#signature nav="簽章原理"}

我們用的演算法是 **Ed25519**（橢圓曲線數位簽章），它有兩把成對的鑰匙：

::: grid2
::: {.card .crit}
#### 私鑰（Private Key）
**只存在於公司內部的簽發站**（`license_center` repo）。
以 passphrase 加密成 PEM 檔存磁碟、權限 `0600`、**絕不進版控**。
能力：**能簽**。
:::
::: {.card .ok}
#### 公鑰（Public Key）
**編譯進產品程式**（`jedi_license_runtime/common/public_keys.py` 的 `PUBLIC_KEYS`）。
不放設定檔、不放 DB——放外面就能被替換。公開出去也無妨。
能力：**只能驗，不能簽**。
:::
:::

這個不對稱是整套機制的支點：

- **客戶偽造不出照**，因為簽名需要私鑰，而私鑰不在他手上、也不在產品裡
- **就算客戶破解了管理員帳號也沒用**——產品裡根本沒有簽發程式碼，只有「收照櫃台」
- **就算公鑰被人拿去**也沒差，公鑰只能拿來驗，驗不出新照

### 為什麼公鑰要編譯進去，不放設定檔

如果公鑰放在 `.env` 或 DB，客戶只要自己產一對鑰匙、把公鑰換成自己的，
就能拿自己簽的假照騙過驗證。編譯進執行檔不是絕對安全（FR-063 的 Nuitka 編譯是為了讓這件事更難），
但把攻擊成本從「改一行設定」拉高到「反編譯改二進位」，這個差距在實務上很有意義。

### `kid`：換鑰的支點

照的信封上寫著一個 `kid`（key id），值是**公鑰的 SHA-256 摘要前 16 個 hex 字元**。
它的作用是告訴驗證端「這張照是用哪把鑰匙簽的」。

驗證端手上的 `PUBLIC_KEYS` 是**一份清單不是一把鑰匙**，收到照先看 `kid` 去清單裡查對應公鑰，
查不到就直接判無效。這樣設計是為了**換鑰時不會一次弄死所有客戶**：
新鑰上線後，舊照帶著舊 `kid` 仍然驗得過（清單裡還留著舊公鑰），新照用新 `kid`，兩者並存，
等舊照全部到期換發完畢，才把舊公鑰從清單移除。

---

## 3. 產生端：一張照是怎麼被簽出來的 {#issue nav="產生端"}

```{.mermaid cap="圖 1 — 簽發管線（License Center 端）"}
%%{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','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TD
  A["授權內容 dict<br/>租戶／方案／人數／模組／到期日／機器指紋…"] --> B["canonical JSON bytes<br/>key 排序、無多餘空白"]
  B --> C["私鑰 Ed25519 簽名"]
  C --> D["signature（base64）"]
  B --> E["zlib 壓縮 → base64<br/>＝ payload"]
  D --> F["v2 信封<br/>{format_version, kid, payload, signature, alg}"]
  E --> F
  F --> G["整包 JSON → base64 → PEM 風格文字塊<br/>-----BEGIN GUIDANT LICENSE-----"]
  G --> H["xxx.license 檔<br/>交付客戶"]
```

每一步的**為什麼**：

### ① canonical JSON：因為簽章簽的是 bytes，不是「意義」

Ed25519 簽的對象是一串**位元組**。同一個 dict 用不同方式序列化會產生不同的 bytes——
key 順序不同、有沒有空白、縮排幾格，出來就是不同的字串，驗簽自然對不上。

所以簽發端與驗證端必須對「怎麼把 dict 變成 bytes」有**逐位元組一致**的約定。
兩邊各自實作（兩個 repo 刻意零依賴），但用的是同一條規則：

```python
json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
```

`sort_keys=True` 固定欄位順序、`separators=(",", ":")` 拿掉所有多餘空白。
這就是 canonical（正規化）的意思——**同一份內容永遠只有一種 bytes 表示法**。
少了這一步，照可能今天簽得出明天驗不過，而且錯得莫名其妙。

### ② 簽的是「解壓後的原文」，不是壓縮後的字串

信封裡的 `payload` 欄位是壓縮打包過的，但**簽章對象始終是解壓後的 canonical bytes**。

這個選擇讓打包層與簽章層徹底解耦：哪天我們想換掉 zlib、改用別的壓縮法、甚至不壓了，
既有已出貨的照仍然驗得過（因為簽的內容沒變）。反過來如果簽的是壓縮後的字串，
換一個壓縮實作、甚至同一個 zlib 換個版本改了預設參數，全世界的照當場失效。

### ③ v2 信封：把該分開的東西分開

```json
{
  "format_version": 2,
  "kid": "a1b2c3d4e5f60718",
  "payload":   "eJxLy8xLVUjOzytJLS4pVgQAKC4E...",
  "signature": "MEUCIQDx3v...",
  "alg": "Ed25519"
}
```

`kid` 與 `signature` 在信封外層是刻意的——驗證端必須**在解開 payload 之前**就知道要用哪把鑰匙。
`format_version` 則是版本演進的閥門：驗證端看到 `2` 走 unwrap 路徑，
看不到這個鍵就當 v1 明文舊照走舊路徑（FR-062 開發期的測試照都是 v1，向下相容是刻意保留的）。

### ④ PEM 外皮（v3）：為了能安全地貼進 email

最外層再包一次，變成這樣的文字塊：

```
-----BEGIN GUIDANT LICENSE-----
eyJmb3JtYXRfdmVyc2lvbiI6Miwia2lkIjoiYTFiMmMzZDRlNWY2MDcxOCIsInBh
eWxvYWQiOiJlSnhMeTh4TFZVak96eXRKTFM0cFZnUUFLQzRFLi4uIn0K
-----END GUIDANT LICENSE-----
```

純外皮層，**內層信封與簽章邏輯完全不動**——`armor_license` 只是「整包 JSON → base64 → 加頭尾包行」。
存在的理由是**交付現實**：照要透過 email 寄給客戶，而 email 會做各種事——
把 `\n` 換成 `\r\n`、在行尾加空白、轉寄時插入引用空行。
裸 JSON 經過這些處理就壞了，PEM 風格的文字塊則天生耐得住（PGP 用了三十年就是這個原因）。
解包端 `dearmor_license` 逐行 strip 後才 join 解碼，正是為了容忍這些污染。

---

## 4. 客戶端：一張照是怎麼被驗開的 {#verify nav="驗證端"}

```{.mermaid cap="圖 2 — 驗證管線（產品端）"}
%%{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','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TD
  A[".license 檔"] --> B["拆 PEM 外皮 dearmor"]
  B --> C["base64 解碼 payload"]
  C --> D["zlib 解壓 → canonical bytes"]
  D --> E["json.loads → payload dict"]
  E --> F{"看信封 kid<br/>查 PUBLIC_KEYS"}
  F -->|查無| X1["拒絕：未知的 kid"]
  F -->|查到公鑰| G{"公鑰驗 signature<br/>對 canonical bytes"}
  G -->|不合| X2["拒絕：簽章驗證失敗（可能遭竄改）"]
  G -->|通過| H{"issued_at 晚於現在？"}
  H -->|是| X3["拒絕：時間異常"]
  H -->|否| I{"host 版：<br/>機器指紋相符？"}
  I -->|不符| X4["拒絕：綁定他機／尚未開通"]
  I -->|相符或 SaaS| J["寫入 DB → 開通完成"]
```

### 🔴 「先解壓再驗簽」的順序不能反，而這正是 CM-1572 的來源 {#cm1572 nav="解壓與驗簽的順序"}

::: {.callout .crit}
**為什麼順序是固定的**

簽章簽的是**解壓後**的 canonical bytes（見上一節 ②）。所以驗證端要驗簽，
就必須**先把 payload 解壓出來**，才有東西可驗。

順序是被簽章對象的選擇決定的，沒有調換空間。

**這個順序的代價**

「先解壓」意味著：**在確認這份檔案是不是真的之前，系統就已經做了一件耗資源的事**。

zlib 有一個放大特性——幾 KB 的壓縮資料可以解出 1GB 的內容（俗稱**壓縮炸彈 / zip bomb**）。
攻擊者只要丟一個精心構造的小檔案到開通端點，系統會乖乖解壓、吃光記憶體，
而「簽章根本不對」這件事要等到解壓完才會被發現——**擋得住的那道門，開在耗資源那一步的後面**。

這就是 FR-076 License 鏈資安掃描抓出的 **CM-1572（壓縮炸彈 DoS）**。
:::

**修法不是調換順序**（順序改不了），而是**在解壓那一步加上限**：

```python
MAX_PAYLOAD_BYTES = 16 * 1024 * 1024        # 16 MiB 解壓上限
MAX_PACKED_BASE64_LEN = MAX_PAYLOAD_BYTES * 2  # 解碼前先看字串長度就擋掉一批
```

16 MiB 這個數字是有來由的：一般 license payload 只有幾百 bytes，
但同一套簽章引擎也在簽 FR-064 的完整性 manifest（全產物逐檔 hash 清單），
實測 v1.19.0b1 已達 912,242 bytes 且會隨產物成長。取實測最大值的約 18 倍，
對正常憑證是天文數字般的餘裕，對攻擊者則把「1GB 放大空間」砍成「16MB」——**記憶體不再可能被無界耗盡**。

而 `MAX_PACKED_BASE64_LEN` 是更前面的一道：base64 編碼會放大約 1.33 倍，
所以光看字串長度就能擋掉多數炸彈，**連 b64decode 都不必做**。這是「把便宜的檢查排在昂貴的操作之前」。

### 開通端點為什麼只擋登入、不擋權限

離線開通（`/license/activation/upload`）與線上開通（`/license/activation/online`）
這兩支端點**任何登入用戶都能打**，不要求管理員角色。這是刻意的設計，不是漏掉守門：

> 開通當下租戶通常還沒有照。而「誰是管理員」這件事在系統裡本身就受 license 管控——
> 要求管理員權限會陷入死鎖：**沒開通就沒管理員、沒管理員就不能開通**。

同理，這兩支端點還必須豁免於「無照唯讀 gate」，否則同樣鎖死。

**但門開著，就代表門後的東西要自己防。** CM-1572 打的正是這裡——
一支免權限的端點接收使用者上傳的資料，並在驗真之前先解壓它。
這不是「權限設計錯了」，而是「權限放寬的地方，輸入處理要更嚴」。

---

## 5. 綁機器：讓照複製到別台機器就失效 {#fingerprint nav="機器指紋"}

**落地版（Host）限定**——SaaS 版的照不綁機器，根本不呼叫這個模組。

### 指紋怎麼算

```
/etc/machine-id            （首選：systemd 標準，Linux 開機安裝時產生）
  ↓ 沒有就退
/var/lib/dbus/machine-id   （較舊發行版的等價來源）
  ↓ 再沒有就退
uuid.getnode()             （MAC 位址推導，僅作本機測試備援）
  ↓
SHA-256 → 64 字元 hex ＝ 機器指紋
```

**為什麼選 machine-id**：它跨重開機穩定，換掉容器、重裝程式、升級版本都不會變，
**只有換機器才會變**——這正是我們要的判準。相對地像 PID、IP、主機名都會隨行程或設定漂移，
拿來綁機器只會讓客戶三天兩頭打電話說「系統突然說我照無效」。

**為什麼取 SHA-256 不直接用原值**：不外洩原始 machine-id，
避免指紋本身變成另一種可回推硬體資訊的洩漏面。

### 怎麼用

照裡寫著一個 `machine_fingerprint` 欄位。開通與啟動時，系統算一次本機指紋跟照裡的比對——
不符就拒絕。**於是把照複製到另一台機器上，驗簽會過（內容沒被改嘛），但指紋核對過不了。**

程式碼裡還特別把「指紋為空」跟「指紋不符」分成兩種錯誤，因為兩者的出路完全相反：

| 情況 | 意思 | 客戶該做什麼 |
|------|------|------------|
| 照裡指紋是**空的** | 新發、尚未開通的照（簽發時還不知道要綁哪台） | 走開通流程（輸序號或離線開通） |
| 照裡指紋**與本機不符** | 這張照綁在別台機器上 | 找 License Center 換綁 |

早期兩者共用同一個錯誤碼，結果 user 實測時拿著未開通的新照，
被訊息誤導成「綁錯機」跑去申請換綁——這才拆成兩碼（CM-1165）。

::: {.callout .warn}
**已知坑：容器裡的 machine-id 可能是空的**

Debian base image 的 `/etc/machine-id` 是**空檔**（不是不存在），
讀出來是空字串，於是退到第三順位的 MAC 推導——而容器 MAC 每次重建都變，
**指紋跟著漂移，照就三天兩頭失效**。

解法是把宿主的 machine-id 掛進容器。細節見
`docs/claude/memory/feedback_container_machine_id_empty_fallback_mac_drifts.md`。
:::

---

## 6. 兩種開通方式 {#activation nav="兩種開通"}

| | **線上開通** | **離線開通** |
|---|---|---|
| **客戶做什麼** | 在系統裡輸入我們給的**開通序號**，按下開通 | 在系統裡**抄下機器碼**，寄給我們；我們簽好照回寄；客戶**上傳照檔** |
| **誰跟簽發站說話** | 客戶的 BE 代打我們的開通伺服器（`LICENSE_ACTIVATION_SERVER_URL`） | 沒有機器連線——人（email）就是傳輸層 |
| **適用情境** | 客戶機器對外網通得到 | 機房完全封閉、air-gapped 環境 |
| **客戶體驗** | 一次按鈕 | 兩趟往返，需要人工協調 |

### 設計要點一：兩條路最後走同一支驗證

線上開通拿回來的照，**照樣走 `activate_license` 做完整驗章 + 指紋核對**。
程式註解寫得直接：

> 開通伺服器已核對序號合法性，但收到照後仍走 `activate_license` 完整驗章＋（host 版）指紋核對，
> **不因來源是開通伺服器就信任**（設計裁示：BE 是唯一信任邊界）。

這條原則的價值在於：驗證邏輯只有一份，不會出現「這條路徑漏檢查了某一項」的縫。
而且「開通伺服器」本身也是網路那頭的東西——中間人、DNS 劫持、設定被改指向假伺服器，
都會讓「來源可信」這個假設破功。唯一穩固的信任錨是**編譯在自己執行檔裡的那把公鑰**。

兩條路唯一的差別只有 `event_type`（`online_activate` vs `offline_activate`），
記在事件時間線上，方便日後追「這張照是怎麼進來的」。

### 設計要點二：兩條路都要能在「還沒有照」的狀態下走通

前面說過的死鎖問題在這裡再強調一次：開通流程的所有端點
（查機器碼、上傳照、線上開通）都必須豁免於 license 執法與唯讀 gate，
否則客戶會卡在「因為沒開通所以不能開通」的迴圈裡。

---

## 7. 時鐘回撥防護：把系統時間調回去也沒用 {#clock nav="時鐘防護"}

**攻擊手法**：照過期了，客戶把伺服器系統時間調回一年前，讓過期照「復活」。

**兩道防線一起擋：**

::: grid2
::: {.card .ok}
#### ① 未來照拒收
`issued_at` 晚於當下系統時間 → 直接拒絕。

擋的是反方向的把戲：把時間調到很後面去換一張「未來簽發」的照，
或拿到一張日期在未來的照想提前用。
:::
::: {.card .ok}
#### ② 浮水印只進不退
DB 裡有一張全域單例表 `public.license_clock_watermark`（恆一列 `id=1`），
記錄系統**見過最晚的 `issued_at`**。

推進用 `GREATEST(max_seen_issued_at, :issued_at)` 語意——
**只會往前，永遠不會倒退**，重跑舊照也推不回去。
:::
:::

驗章時拿當下的 `issued_at` 跟浮水印比：早於浮水印**超過 24 小時容差**就判定時鐘被回撥。
留容差（`CLOCK_ROLLBACK_TOLERANCE`）是因為 NTP 校時、時區設定、虛擬機掛起恢復都可能造成小幅偏移，
零容差會把正常客戶誤傷成攻擊者。

浮水印的讀寫刻意放在 infra 層（`clock_watermark_repo.py`），驗章引擎維持純函式不碰 DB——
這樣同一支 `verify_payload` 才能同時服務 license、manifest、unlock_token 三種憑證，
其中有些（例如啟動 gate 的完整性驗證）跑在 `create_app()` 之前、根本還沒有 DB 連線。

---

## 8. 這套機制的邊界（誠實段） {#boundary nav="邊界"}

::: {.callout .crit}
**落地版客戶手上有整包程式碼，理論上可以反編譯、把驗證邏輯整段拿掉——這套機制擋不住，
而且沒有任何純軟體方案擋得住。**
:::

這不是我們做得不夠好，是這類問題的本質：
**當執行環境完全在對方控制之下，任何「在對方機器上執行的檢查」原則上都可以被繞過。**
DRM 產業投入的資源比我們高幾個數量級，結論也是一樣的。

所以定位要講清楚：

| 這套機制**防得住** | 這套機制**防不住** |
|---|---|
| 無意識越權（不知道自己沒買這個模組） | 專業破解（反編譯、patch 二進位） |
| 順手改一下（把到期日改個數字、多加幾個人） | 有決心且有能力的內部人員 |
| 照被複製到別台機器繼續用 | — |
| 系統時間被調回去讓過期照復活 | — |
| 客戶自己簽一張假照 | — |

**真正的防線是合約與商業關係**，技術機制的角色是：
① 讓「順手改一下」這件事不可能發生（改了就驗不過，沒有灰色地帶）；
② 竄改時**留下證據**（本地事件記錄 + 檔案指紋），成為舉證材料。

FR-063（Nuitka 編譯）與 FR-064（防竄改偵測）都是**提高門檻**，不是提供保證。
把攻擊成本從「五分鐘改設定」拉到「需要逆向工程能力與數天工時」，
在商業上已經足以讓絕大多數人選擇正常付費——這就是這類機制實際能兌現的價值。

design.md §4.2 的防線分層寫得很清楚，值得再引一次：

> 簽章防竄改 → 公鑰編譯進執行檔防替換 → 時鐘回撥偵測 → 蓄意破解（patch 產品程式碼）靠合約與稽核約束，
> 技術上留存竄改證據作舉證材料。**破解管理員帳號也無法自行簽發——產品內沒有私鑰、沒有簽發程式碼，
> 只有「收照櫃台」。**

---

## 9. 事實來源與延伸閱讀 {#sources nav="事實來源"}

| 內容 | 檔案 |
|------|------|
| 簽章引擎、v2 信封、PEM 外皮、canonical JSON | `license_center/src/license_center/core/crypto/engine.py`（檔頭 docstring 資訊量最大） |
| 金鑰管理、`kid` 計算、passphrase 存檔 | `license_center/src/license_center/core/crypto/keys.py` |
| 客戶端驗章（含解壓上限） | `jedi-license-runtime/jedi_license_runtime/common/engine.py`、`common/constant.py` |
| 機器指紋 | `jedi-license-runtime/jedi_license_runtime/common/machine_fingerprint.py` |
| 兩種開通端點 | `jedi-license-runtime/jedi_license_runtime/api/license_route.py` |
| 開通與驗證的商務流程 | `jedi-license-runtime/jedi_license_runtime/app/service/license_verification_service.py` |
| 時鐘浮水印 | `jedi-license-runtime/jedi_license_runtime/infra/clock_watermark_repo.py` |
| 規格條列（本文的上游） | [`design.html`](./design.html) §4.1 照檔格式 / §4.2 簽章與驗證 / §4.9 金鑰生命週期 |
| 到期管線、Plan、模組授權等其他面向 | [`design.html`](./design.html) §4.3–§4.10 |
