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 殼)、24834d73aeb2bd44(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 前的既有待辦仍在