# FR-064 防竄改偵測與授權鎖定 — 收尾 SUMMARY

> **母案 CM-1188** ｜ 收尾日 2026-08-16 ｜ 三階段落地版戰線第二棒（FR-063 編譯 ✅ → **FR-064 防竄改【本案】** → FR-065 Installer）
> 交接軌跡見同資料夾 `FR-064-LOG.md`（9 棒 append-only）＋ `FR-064-STATE.md`（living 現況）。

## 1. 做了什麼（一句話）

客戶機上偵測 Nuitka 產物被竄改 → 服務立即終結＋鎖定，唯一解鎖＝原廠一次性 unlock token。目標「99% 不值得動手＋1% 留蓄意痕跡」，B2B 法律層是真防線；離線必須成立。

## 2. 機制全貌（端到端）

```
build 期逐檔 SHA-256 分層 manifest（core/resources/thirdparty）
  → LC Ed25519 簽章（復用 FR-062，泛用 sign_payload/verify_payload，payload_type 判別、缺鍵視為 license 保向後相容）
  → 啟動 gate 驗章＋比對 core+resources（1304 檔）→ 不符即拒啟
  → runtime 4h+jitter 抽查＋業務埋點（license 匯入／SSP 匯出）
  → 偵測 tamper → 服務死透 → 雙落點（FS 標記＋DB 單例表）＋D9 best-effort 回報 LC
  → 重啟被 gate 攔 → 起 lockdown 殼（回 503 INTEGRITY_503001，帶 fingerprint/event_id）
  → FE interceptor 認碼 → 整站跳鎖定頁
  → 解鎖：客戶抄 fingerprint+event_id 給原廠 → LC 簽 unlock token → 放檔 restart → 核銷四關 → 解除
```

## 3. Commits 清單（三 repo，皆已 push 與 origin 同步）

### BE（`feature/FR-064`）主要 commits
- 簽驗共用層/manifest/gate/落點/抽查/埋點：`97bca82e`（verify_payload）、`eba7f5a0`/`d4ef0285`/`607b4b3a`（gate＋雙落點）、`b4a102b7`（抽查＋埋點）、build_release manifest 系列
- 唯讀化：`7079ae24`/`3f338ffc`（T-6.1/6.2）、`d181fdc6`（D9 回報）
- unlock 全鏈：`f8133e72`（核銷四關＋回執回填 DB）
- D11 殼＋forensic：`520fc5e3`（lockdown 殼）、`24834d73`＋`aeb2bd44`（forensic 擴充）、`e1190f86`（locale C.UTF-8）、`4e64ca07`（殼協商改只看 Accept）
- 部署定式：`2d56edb6`（docker-compose，含 pki volume 補洞）、`f929abe7`（build_all.sh）、`4c06ddc8`（scripts 盤點）
- FE details 開關（跨 repo）：FE `29fead0`（CM-1240）
- **兩張 P0 修復**：`7cc7989e`（BUG-1 死透，killpg 打不到 PID 1 改依承載送訊號）、`19a086af`（BUG-2 指紋，掛宿主 /etc/machine-id）

### LC（`feature/FR-064`）
- `6ce339e`（sign_payload 泛用層）、`57c6566`（manifest 簽章）、`651db9c`（unlock token 簽發：後台頁＋CLI）、`7c2974d`（tamper 事件接收 D9）、`076c06b`（部署四步 SOP）

### FE（`feature/FR-064`）
- `de23772`（鎖定頁＋interceptor）、`29fead0`（指紋/時間預設隱藏，?details=1 才顯示）

## 4. 決策定案（D1–D11，全文 design.md §2）

| # | 一句話 |
|---|--------|
| D1 | 分層驗證：gate 全驗 core+resources、thirdparty 只由 runtime 抽查覆蓋、volume 絕不入清單 |
| D2 | 簽驗共用層：LC/BE 抽泛用 sign/verify_payload，license 向後相容（缺 payload_type 視為 license） |
| D3 | 啟動 gate 錨點＝main.py validate_required_env 之後、create_app 之前，無 DB/DI 依賴 |
| D4 | /app 唯讀化：產物 root:root＋進程 uid 1000＋compose read_only |
| D5 | tamper 雙落點 FS+DB，任一存在即拒啟；已知限制（重建+還原可洗）明文承認 |
| D6 | unlock 離線信封：LC 簽 {machine_fingerprint, tamper_event_id, nonce, expires_at}，一次性綁機 |
| D7 | 驗證邊界承認：manifest 自身不入清單、image 層驗證劃歸 FR-065 |
| D8 | 抽查 4h+jitter 0-60min，參數寫死不開放（可設定＝可關閉） |
| D9 | best-effort 回報 LC（發不出不重試不阻塞），補 D5 本地可洗的洞 |
| D10 | 抽查二次確認：hash 不符立即複算，兩次皆不符才判 tamper（排除 I/O 毛刺） |
| D11 | lockdown 殼（B 方案）＋forensic 證據規格＋「行為人不可知」邊界明文 |

## 5. 行為差異（部署後 vs 之前）

- **之前**：產物可任意改，服務照跑，無感
- **之後**：改 core/resources 任一檔 → 啟動即拒（gate）；運行中改 → 服務死透 → 重啟進鎖定殼；客戶端看到正式鎖定頁＋事件編號（非 timeout/connection refused）；解鎖需原廠離線簽發的一次性 token

## 6. 兩個出貨阻斷 P0（本 session 抓到並修復）

### BUG-1（CM-1241）hook 觸發 tamper 後 gunicorn master respawn 不死透
- **根因**：`killpg(1, sig)`＝`kill(-1, sig)` POSIX 廣播特例，Linux 明文把 PID 1 排除 → master 根本沒收到訊號（非「收到卻不理」，推翻原 docstring 假設）
- **修法**：`_terminate_process` 改依承載分流——gunicorn worker 指名對 master 送 SIGQUIT（有 handler，quick shutdown 不 respawn）、單 process/PID 1 直接 os._exit、bare metal 僅 pgid==pid 才 killpg（不傷 shell）；master pid 由 post_fork 登記不用猜
- **驗證**：真 image C-2 死透（log 見 `[1] Handling signal: quit → Shutting down: Master`）、C-1 scheduler 路徑不同病、突變測試×3

### BUG-2（CM-1242）容器機器指紋每次重啟就變
- **根因**：Debian base image 的 `/etc/machine-id` 是 0 bytes 空檔（容器不跑 systemd 永遠填不上）→ 退回 `uuid.getnode()`（容器 MAC，docker restart 就換）
- **影響**：unlock token 綁的指紋一重啟即失效（永遠解不了鎖）；host 版 license 綁機同漂
- **修法**：docker-compose 掛 `/etc/machine-id:/etc/machine-id:ro`，容器指紋綁宿主機（code 不動）
- **驗證**：指紋跨 restart／換容器不變（錨點 sha256(宿主 machine-id) 對上）；A-4 unlock 全鏈＋4 負面案；既有 license 綁機評估無回歸（現行 host 照無一綁會漂指紋）

## 7. 真 image 端到端實證（188 SaaS 部署 + host 模式驗證）

- 188 正式容器 image 1.14.0（含兩 P0）、compose 起、read_only 生效、掛 machine-id、gate 通過 1304 檔、兩容器 healthy
- 竄改→死透→重啟進殼→503 整鏈：真 image 通過
- unlock 解鎖循環：LC 簽 token → 核銷四關 → 解除 → receipt/nonce/DB unlocked_at 回填全鏈通過
- 三承載終止機制：gunicorn worker（SIGQUIT master）／單 process（os._exit）／scheduler（os._exit）

## 8. 規範文件清單（已更新）

- `design.md` §2 D1–D11、§4.4（D11 殼＋forensic）
- `docs/user-manual/system-integrity-lockdown-guide.md`（客戶側解鎖手冊，含 ?details=1 取指紋）
- `docs/features/.../unlock-sop.md`（原廠側簽發 SOP）
- `deployment-env.md`（machine-id 掛載＋read_only）
- LC `CLAUDE.md`（部署四步）

## 9. 已知限制 / follow-up（明文，不影響出貨判定）

1. 容器重建＋DB 還原可洗掉本地紀錄（D5 明文承認；D9 在線回報補洞）
2. **K8s liveness 下鎖定殼會 CrashLoop**——compose 落地版不受影響，FR-065 若上 K8s 需把鎖定態排除在 liveness 之外
3. thirdparty 4h 真窗未長跑驗（縮短間隔驗過）、≥24h 零誤報未長跑
4. socketio 真 image 完整鏈用 harness 代驗（真源碼、非真 image binary）
5. 宿主無 /etc/machine-id 時 docker 建目錄行為未實測（compose 註解已警示）
6. Nuitka 產物下埋點 `_HOTPATH_PRIORITY_PREFIXES` 全編進 binary，埋點實際只驗 binary 一檔（行為認知修正，非 bug）

## 10. 部署 handover

- 三 repo `feature/FR-064` 皆與 origin 同步
- 188 已用 `docker compose -f docker/production/docker-compose.yml up -d` 起正式容器（含兩 P0）
- 換版：改 GUIDANT_VERSION 或 pull 新 image 後重跑 up -d
- **未進版**（現標 1.14.0）——FR-064 出貨里程碑確定時走 version-bump skill；後續尚有 FR-065 installer
- **未來若正式出貨**：image 尚未推 Harbor（本機 tag 已打）；正式簽章鑰 PUBLIC_KEYS 進 image 前的既有待辦仍在
