FR-064 · Tamper Detection — 設計定案 · 2026-08-15
設計定案版:D1–D10 全數拍板(2026-08-15),D11 補充定案(2026-08-16)。Build 期逐檔 SHA-256 manifest 以原廠私鑰簽章(復用 FR-062 LC 簽驗體系,抽泛用 sign_payload/verify_payload 共用層);啟動 gate 分層驗證(①業務+②資料檔全驗,③第三方交 runtime 抽查);偵測竄改 → tamper 雙落點+起 lockdown 殼鎖定服務(D11,取代純 exit),之後每次啟動視同無 license 拒啟;唯一解鎖=原廠一次性 unlock token(離線信封,air-gapped 成立)。加碼:/app 唯讀化(D4)、在線 best-effort 回報 LC(D9)、抽查二次確認防毛刺(D10)、forensic 存證擴充與主機層審計邊界明文(D11)。
狀態:設計定案(D1–D10 已拍板)|建立日期:2026-08-15|FR-063 Nuitka 編譯續作(三階段落地版第二棒) 討論稿(含現況盤點全文):
discussion.html|Notion 母案 CM-1188
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-08-15 | 初版設計定案:討論稿 D1–D8 拍板(D1 採分層 A;D8 抽查週期由原提案 45min 調整為 4h+jitter 0–60min,並加業務路徑輕量埋點);新增 D9(在線 best-effort 回報 LC)、D10(抽查二次確認)兩項決策一併定案 | FR-064 母案 CM-1188 |
| 2026-08-16 | 新增 D11:鎖定溝通改採 lockdown 殼(B 方案,取代純 exit)+forensic 存證 snapshot 擴充+「是誰改的」邊界明文與契約措辭建議;§4.4 全段改寫;§5 新增 FR-064.8 段(T-8.1–T-8.4,本卡 CM-1234 為 T-8.4) | CM-1234 |
本案是「三階段落地版」戰線的第二棒(Notion 母案 CM-1188):
| 棒次 | 案號 | 內容 | 狀態 |
|---|---|---|---|
| 第一棒 | FR-063 | Nuitka 編譯——整套 BE 編成機器碼出貨,產物 Harbor guidant-ai-be:1.14.0(bake cc7714d2) |
已完結 |
| 第二棒 | FR-064 | **防竄改偵測(本案)——偵測產物被竄改 → 終結服務並鎖定產品;唯一解鎖=原廠一次性 unlock token | 設計定案** |
| 第三棒 | FR-065 | Installer——客戶端安裝/升級體驗;image 層簽章驗證(cosign/digest)劃歸此棒 | 未開 |
FR-063 解決「原始碼不外流」,本案解決「機器碼被動手腳怎麼辦」——兩者合起來才構成落地版的完整保護面。
偵測程式被竄改 → 終結服務並鎖定產品;唯一解鎖 = 原廠簽發的一次性 unlock token。
四個動詞:偵測(build 期簽章 manifest + 啟動驗證 + runtime 抽查 + 業務埋點)、終結(偵測即 exit,不降級不告警後續跑)、鎖定(tamper 雙落點,之後啟動視同無 license 拒啟)、解鎖(原廠一次性 unlock token,離線信封)。
這不是 DRM 軍備競賽,是「拉高成本+留下痕跡」
client-side 完整性驗證無法對抗有決心的逆向團隊——驗證邏輯本身就在客戶手上的機器碼裡。目標定位:
業界對照定位:Linux 生態沒有 OS 級 codesign 強制執行;容器層 image 簽章(cosign/digest pinning)明確劃歸 FR-065 installer。本案=應用層 self-integrity check,是 B2B 落地軟體的標準做法;縱深防禦 = 部署時驗 image(FR-065)+ 運行時 self-check(本案)。
全鏈:build 簽章 → 啟動驗證 → runtime 抽查+業務埋點 → tamper 鎖定(+best-effort 回報 LC)→ unlock 解鎖。
%%{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'}}}%%
sequenceDiagram
participant BLD as 原廠 build 管線
participant LC as License Center(私鑰)
participant APP as 產品 binary(客戶端)
participant SCH as APScheduler 抽查 job
participant ST as tamper 落點(DB+FS)
Note over BLD,LC: Build 期(原廠側)
BLD->>BLD: Step 4 尾端:dist 定案後逐檔 SHA-256 → manifest
BLD->>LC: sign_payload(manifest, type=manifest)
LC-->>BLD: 簽章信封(kid+signature)
BLD->>BLD: manifest+信封放入 dist → build_image → Harbor
Note over APP,ST: 啟動期(客戶端,每次啟動)
APP->>ST: 查 tamper 紀錄(DB+FS 任一存在?)
alt 已有 tamper 紀錄
ST-->>APP: 存在
APP->>APP: 視同無 license → 拒啟 exit
else 無紀錄
APP->>APP: verify_payload(manifest 信封)(硬編公鑰,無 DB 依賴)
APP->>APP: 逐檔比對 ①業務+②資料檔(D1 分層)
alt hash 不符或驗章失敗
APP->>ST: 寫入 tamper 事件(雙落點)
APP->>LC: best-effort 回報事件(D9,發不出不重試)
APP->>APP: 立即 exit
else 全部通過
APP->>APP: create_app() → 正常服務
end
end
Note over SCH,ST: Runtime(低頻抽查+業務埋點)
loop 每 4h+jitter 0–60min(D8,每輪重抽)
SCH->>SCH: 固定核心(①②全量)+隨機 N 個③層檔重算 hash
alt 單檔不符
SCH->>SCH: 立即複算同檔一次(D10 二次確認)
alt 兩次皆不符
SCH->>ST: 寫入 tamper 事件(雙落點)
SCH->>LC: best-effort 回報事件(D9)
SCH->>APP: 立即 exit(終結服務)
end
end
end
Note over LC,APP: 解鎖(唯一途徑,離線成立)
APP-->>LC: 客戶回報 machine_fingerprint+tamper_event_id(電話/郵件等人工通道)
LC->>LC: 簽發 unlock token{type:unlock, fingerprint, event_id, nonce, expires_at}
LC-->>APP: token 檔交付(離線可攜)
APP->>APP: verify_payload(token)+比對 fingerprint+核對 event_id+查本地已用 nonce
APP->>ST: 清除 tamper 紀錄+記錄 nonce 已用(防重放)
APP->>APP: 下次啟動恢復正常驗證流程
%%{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 LR
subgraph FACTORY["原廠側"]
direction TB
subgraph LCBOX["License Center(獨立 repo/DB)"]
SP["sign_payload 泛用簽章層(D2 新抽)<br/>type: license / manifest / unlock_token"]
PK["Ed25519 私鑰<br/>PKCS8+passphrase"]
UT["unlock token 簽發(D6)<br/>傾向後台頁(62.10 internal API 慣例)"]
RCV["tamper 事件接收端點(D9 新增)<br/>62.10 internal API 形狀"]
SP --- PK
UT --> SP
end
subgraph BUILDBOX["build 管線(build_release.sh)"]
MF["Step 4 尾端:<br/>逐檔 SHA-256 → manifest<br/>排除規則=rsync 排除"]
MF -->|"送簽"| SP
IMG["build_image → Harbor<br/>manifest+信封入 dist"]
MF --> IMG
end
end
subgraph CLIENT["客戶側(產品 binary,可完全離線)"]
direction TB
VP["verify_payload 泛用驗章層(D2)<br/>公鑰硬編 public_keys.py(D3)"]
BOOT["啟動 gate(main.py)<br/>env 驗證後、create_app 前<br/>①②層全驗(D1)"]
RT["APScheduler 抽查 job(D8)<br/>4h+jitter;固定核心+隨機 N<br/>api/socketio 兩模式都跑"]
HP["業務埋點 2–3 處(D8)<br/>license 匯入/報告匯出順驗核心小集合"]
TS["tamper 落點(D5)<br/>DB 全域單例表+FS volume 檔"]
RO["/app 唯讀化(D4)<br/>chown root:root+--read-only"]
UV["unlock 驗證(D6)<br/>fingerprint+event_id+nonce 防重放"]
BOOT --> VP
RT --> VP
HP --> VP
BOOT -->|"偵測竄改"| TS
RT -->|"D10 二次確認後"| TS
HP -->|"偵測竄改"| TS
TS -->|"存在即拒啟"| BOOT
UV -->|"清除紀錄"| TS
UV --> VP
end
IMG -.->|"image 出貨"| CLIENT
UT -.->|"token 檔離線交付"| UV
TS -.->|"best-effort 回報(D9)"| RCV
共用一把鑰匙、一條 code path:sign_payload / verify_payload 是 D2 抽出的泛用層——license、manifest、unlock token 三種憑證走同一簽驗實作、同一把公鑰。拆掉驗證的人等於同時拆掉 license,「拆一個等於拆兩個」。
2026-08-15 決策者全數拍板。每項含定案內容、理由、被排除方案。
| # | 決策 | 定案 |
|---|---|---|
| D1 | 保護範圍分層 | 採 A 分層驗證。啟動 gate 全驗①業務 .so+②隨版資料檔(數百~千餘檔,秒級可接受);③第三方 2–4 萬檔全部納 manifest 但只由 runtime 抽查覆蓋;④volume 掛載絕不入清單(客戶正常寫入面,是唯一系統性誤報源)。排除 B 全量啟動驗——啟動多付十數秒 I/O 換不到等值防護(第三方 .py 攻擊價值低於業務碼);排除 C 只驗業務——資料檔(模板/canon json)被改會影響產出正確性而完全無感。 |
| D2 | 簽驗共用層 | 准。LC+BE 兩端抽泛用 sign_payload(dict)→envelope / verify_payload(envelope)→dict,payload 帶 type 欄(license / manifest / unlock);三種憑證共用同一引擎、同一把公鑰——這是「拆一個等於拆兩個」防護敘事的實作前提,不抽就是第二套簽驗實作(違反不重複造輪子鐵則)。約束:license 既有簽發/驗證必須跑完整迴歸;已出貨 license 檔格式向後相容(v2 信封/v3 armor 不變)。 |
| D3 | 啟動驗證錨點 | main.py validate_required_env() 之後、create_app() 之前。同層級、同 fail-fast 語氣(一次列全、raise 終止);公鑰沿用 common/license/public_keys.py 硬編慣例(隨 binary 編譯);驗證無 DB/DI 依賴——tamper 歷史檢查此時讀檔案落點,DB 落點起服務後補寫。排除更早錨點(load_dotenv 前)——驗不到更多東西還放棄 env 設定能力,收益低。 |
| D4 | /app 唯讀化 | 做。entrypoint 改產物歸 root:root、進程跑 uid 1000——檔案系統層直接寫不進(現況 /app owner=執行者本人,竄改門檻是零);部署文件建議加 docker --read-only。實作前置驗證:確認 Nuitka binary 與③層附帶套件 runtime 無寫產物目錄行為(③層有 .py,__pycache__ 寫入疑慮)——若發現寫入需求需回報調整方案,不硬上。 |
| D5 | tamper 雙落點 | DB+檔案系統雙落點。DB=新全域單例表(比照 license_clock_watermark 形狀:public schema、不掛 RLS、cm_app 可寫——tamper 是機器級不是租戶級);FS=持久 volume(/opt/guidant/pki 或同級)標記檔。任一存在即拒啟。已知限制明文寫入:容器重建+DB 還原可洗掉本地紀錄——在「拉高成本+留痕跡」定位下接受(會做到這步的人已屬蓄意,蓄意本身在 B2B 合約場景就是求償事由);且 D9 在線回報使有網環境的紀錄存於 LC 端不可洗,補上這個洞。 |
| D6 | unlock token 離線信封 | LC 私鑰簽 {type:unlock, machine_fingerprint, tamper_event_id, nonce, expires_at(短效)}。產品端離線驗章+比對本機指紋(compute_machine_fingerprint() 現成)+核對 tamper_event_id+本地記已用 nonce 防重放;一次性、不可跨機。誤鎖與真竄改走同一路徑(原廠人工研判後簽發)。排除 LC activation_code 線上三件套模式——被鎖機器可能連不到 LC,必須離線成立。LC 端簽發形式(CLI vs 後台頁)留實作棒依 LC 既有形狀選,傾向後台頁沿用 62.10 internal API 慣例。 |
| D7 | 驗證邊界(承認) | manifest+簽章檔放 dist 內、自身不入清單(驗不了自己);entrypoint/Dockerfile/image 層不在 client 端驗證範圍——binary 外的東西沒有可信錨點。防線=D4 唯讀化+驗證邏輯本身是機器碼+檢查點分散互驗。image 簽章驗證(cosign/digest pinning)明確劃歸 FR-065 installer,不在本案硬撐。 |
| D8 | 抽查參數 | 週期=4 小時+每輪隨機 jitter 0–60 分(每輪重抽亂數,防節奏可預測);每輪內容=固定核心(①②全量)+隨機 N 個③層檔;業務路徑輕量埋點 2–3 處(如 license 匯入、報告匯出時順驗核心小集合,毫秒級——把檢查時機綁使用者行為而非時鐘);api/socketio 兩模式都跑;參數寫死不開放設定(可設定=可關閉,env 是客戶可控面)。排除原提案 45min——過頻:抽查是事後抓不是即時擋,時效差數小時防護價值幾乎無差;「改完改回」型攻擊本就靠 D4+啟動 gate 抓,不是抽查的任務。 |
| D9 | 在線 best-effort 回報 LC(新增) | 做。偵測 tamper 時嘗試把事件(machine_fingerprint/事件ID/檔案/時間/hash 差異)回報 LC 留存——發得出就發、發不出不重試、不阻塞、不影響鎖定流程;LC 加接收端點(走 62.10 internal API 形狀)。價值:在線環境的 tamper 紀錄存於客戶碰不到的 LC 端,補 D5「本地紀錄可洗掉」的洞。 |
| D10 | 抽查二次確認(新增) | 做。runtime 抽查發現 hash 不符 → 立即複算同檔一次,兩次皆不符才判 tamper(排除 I/O 瞬間毛刺造成誤鎖);啟動 gate 不需此緩衝(啟動期無併發 I/O 干擾,且拒啟成本遠低於運行中誤殺)。 |
| D11 | 鎖定溝通機制+forensic 證據規格(新增,2026-08-16) | 採 B 方案:lockdown 殼。gate 判定拒啟時不直接 exit,改起 stdlib 極簡 HTTP responder(無 Flask/DI/DB,零業務功能、零寫入面)——任何請求一律回鎖定資訊 HTML+JSON(error_code=INTEGRITY_503001,data 帶 machine_fingerprint/tamper_event_id/detected_at);FE 認得該 error_code 即整站導向鎖定頁。unlock 核銷不走殼,維持既有 FS token 檔核銷流程(D6)。同時定案:forensic 證據規格擴充——tamper 事件不符檔案各帶 mtime/ctime/owner/size(mtime 是最接近竄改行為時間點的證據);觸發脈絡依偵測來源分級 best-effort 蒐集:業務埋點→當下登入帳號+來源 IP;runtime 抽查→活躍 session 快照(帳號/IP/最後活動時間);boot gate→僅檔案 metadata+app.log 尾段(此時無使用者 session 可查);三落點(FS 標記檔/DB integrity_tamper_events/D9 回報 LC)同步吃擴充欄位。邊界明文:「是誰改了檔案」在應用層不可知——竄改動作走 SSH/docker exec 等主機層管道,不經過產品業務邏輯,產品拿不到操作者身分;forensic 擴充蒐集的是「檔案/時間/(若有)業務 session」證據,不是「竄改者是誰」的答案。理由:web 產品純 exit 時使用者只見連線失敗(timeout/connection refused),會被歸因為產品品質問題而非安全事件;lockdown 頁正式呈現 tamper_event_id 給客戶,同時滿足「留痕跡」與「B2B 法律敘事」——契約條款應綁「產品完整性驗證失敗即構成違約事由」,而非「查明竄改行為人」,前者產品證據鏈自足可主張,後者舉證做不到。排除方案:A(純 exit,原 4.4 版本)——使用者體感等同故障,無法承載留痕跡與法律敘事的定位;C(半套:僅寫 log 不起殼,靠人工事後翻 log 告知客戶)——鎖定當下客戶端零可見資訊,錯失「當場出示 event_id」的證據呈現時機,且仰賴原廠事後人工介入才看得到,不符「產品自足證據鏈」目標。 |
以下事實皆經實碼/實測驗證(詳見討論稿 §2),是本設計的基礎。
| 元件 | 座標 | 現況 → FR-064 動作 |
|---|---|---|
| 簽章演算法 | Ed25519,canonical JSON bytes(sort_keys+緊湊分隔);LC src/license_center/core/crypto/engine.py:56 sign_license()、BE common/license/engine.py:90 逐位元組一致 |
沿用,抽泛用層(D2) |
| kid | 公鑰 SHA-256 hex 前 16 chars(keys.py:21 compute_kid()) |
沿用;跨版本共用公鑰,kid 輪替機制既有 |
| 私鑰 | PKCS8 PEM+passphrase(env LICENSE_CENTER_KEY_PASSPHRASE;air-gapped 回退表單輸入);LC 端持有,產品端永不落地 |
沿用 |
| 信封格式 | v2 信封(kid/payload zlib+b64/signature)+v3 armor,兩端互通 | 沿用;license 檔格式向後相容(D2 約束) |
| 產品端公鑰 | common/license/public_keys.py 硬編源碼(現三把 DEV/STG/POC),隨 binary 編譯 |
沿用(D3) |
| 驗章核心 | common/license/engine.py:138 verify_license() 純函式,但型別綁死 License 18 商務 key |
缺口:抽 sign_payload/verify_payload 泛用層,LC+BE 兩端都動(D2) |
| 機器指紋 | common/license/machine_fingerprint.py compute_machine_fingerprint() |
現成,unlock token 直接用(D6) |
| 一次性碼借鏡 | LC activation_code 三件套(一次性/activated_at 判已用/重用 409) |
模式可抄,但它是線上模型——unlock 改離線信封(D6) |
| 執法慣例 | fail-closed;總開關 LICENSE_ENFORCEMENT_ENABLED;驗失敗 log 留證 |
沿用同語氣;「表格化留證」當時註記留給後續階段——FR-064 正是該階段 |
build 管線:scripts/build/build_release.sh 四步(deps → resources → freeze → compile+斷言)。manifest 產生插入點:Step 4 尾端、全部斷言通過之後、進 build_image 之前——dist 定案的時刻。
🔴 manifest 排除規則必須與 build_image.sh 的 rsync 排除一致
build_image.sh 用 rsync 複製另一份 context(排除 .env* / log / static / smoke-*.log / __pycache__)。排除規則不一致 = 簽了進不了 image 的檔、或 image 裡有沒簽的檔,啟動驗證必對不上。manifest+簽章檔要一併帶進 context。
產物四層(直接決定 D1 分層):
| 層 | 內容 | 規模 | 竄改攻擊價值 | 驗證覆蓋 |
|---|---|---|---|---|
| ① 業務資產 | binary 本體+Nuitka .so(零 .py) | 數百檔 | 最高 | 啟動 gate 全驗+抽查固定核心+業務埋點 |
| ② 隨版資料檔 | translations .mo、ssp_cmmc_template.docx、cmmc canon json ×2、resources/download 5 檔、report templates html |
數百~千餘檔 | 中 | 啟動 gate 全驗+抽查固定核心 |
| ③ 第三方原樣附帶 | EXCLUDE_COMPILE_PKGS 11 支+相依封閉集合+dist-info+.libs | 2–4 萬檔,.py 佔大宗 | 低(但最易下手) | 全納 manifest,runtime 隨機抽查長期覆蓋 |
| ④ volume | /app/static /app/log /home/guidant /opt/guidant/pki /tmp |
動態 | — | 絕不入清單(唯一系統性誤報源) |
main.py):load_dotenv → RUN_MODE 解析 → validate_required_env()(缺項一次列全 raise)→ create_app() → REGISTERED_APPS 逐一 import。啟動 gate 掛 validate_required_env() 之後、create_app() 之前(D3)。core/scheduler.py init_scheduler):api 模式現有 4 jobs,既有 pattern=app_context → DI container → session_scope。完整性抽查 job 是唯一不需 DB/DI 的 job,可省 container。新增 job 要同步 L173 啟動摘要字串與 main.py 檔頭模式裁剪表。COPY --chown=1000:1000、entrypoint root 起 chown 四掛載點後 setpriv 降 1000——/app owner=執行者本人,產物可寫、竄改門檻為零。這是 D4 的直接動機(唯讀化擋事前、驗證抓事後,互補)。// integrity-manifest.json(信封 payload 解開後的形狀)
{
"type": "manifest",
"product": "guidant-ai-be",
"version": "1.15.0",
"built_at": "2026-08-20T00:00:00Z",
"layers": {
"core": {"files": {"guidant-ai-be": "sha256hex", "app/....so": "..."}},
"resources": {"files": {"config/translations/....mo": "..."}},
"thirdparty": {"files": {"pandas/....py": "..."}}
}
}integrity-manifest.sig)。layers 分層服務 D1:啟動 gate 只吃 core+resources,抽查 job 吃全部。DB:新全域單例語意表(比照 license_clock_watermark:public schema、不掛 RLS、cm_app 可寫):
-- 草案形狀,最終 DDL 依 sql-migration skill 落地
CREATE TABLE public.integrity_tamper_events (
id BIGSERIAL PRIMARY KEY,
event_uid VARCHAR(64) NOT NULL UNIQUE, -- unlock token 綁定用
detected_at TIMESTAMPTZ NOT NULL,
detected_by VARCHAR(16) NOT NULL, -- boot | scheduler | hook
machine_fingerprint VARCHAR(128) NOT NULL,
detail JSONB NOT NULL, -- 檔案路徑、預期/實際 hash、驗章 reason
unlocked_at TIMESTAMPTZ, -- unlock 成功時間;NULL=仍鎖定
unlock_nonce VARCHAR(64) -- 已用 nonce 記錄(防重放)
);檔案系統:持久 volume(/opt/guidant/pki 或同級)下的標記檔,內容含 event_uid+detected_at+detail 摘要。任一存在即拒啟——DB 被還原時 FS 仍在,FS 被刪時 DB 仍在。
寫入時序(配合 D3 無 DB 依賴):啟動 gate 偵測竄改時先寫 FS 落點再 exit;DB 落點由「下次任何成功起服務的路徑」不存在——故 boot 偵測的事件以 FS 為主落點,scheduler/埋點偵測(服務已起、DB 可用)雙落點同時寫。啟動期 tamper 歷史檢查讀 FS;服務起來後(scheduler 首輪)將 FS 紀錄補同步進 DB。
已知限制(明文承認,D5):容器重建+DB 還原可洗掉本地紀錄。「拉高成本留痕跡」定位下接受;D9 在線回報使有網環境的紀錄存於 LC 端不可洗。
{
"type": "unlock_token",
"machine_fingerprint": "…", // 必須與被鎖機器一致(不可跨機)
"tamper_event_id": "event_uid", // 綁定特定事件
"nonce": "隨機 64 hex", // 一次性,用過即記錄
"issued_at": "…",
"expires_at": "…" // 短效期(如 72h)
}走 v2/v3 信封+LC 私鑰簽章。產品端核銷四關:verify_payload 驗章 → 比對本機 fingerprint → 核對 tamper_event_id → 查本地已用 nonce——全過才清除 tamper 紀錄(FS+DB)並記錄 nonce 已用。誤鎖與真竄改走同一路徑(原廠人工研判後簽發)。LC 端簽發介面留實作棒依 LC 既有形狀選,傾向後台頁(62.10 internal API 慣例)。
原設計(D3/D5)偵測竄改後直接 exit——容器停掉、連線失敗。問題在於 web 產品的使用者看不到「服務被鎖定」這件事,只看到打不開:瀏覽器顯示 timeout/connection refused,這在使用者認知裡與「產品當機」「網路問題」「伺服器爆掉」無法區分。對 B2B 客戶而言,這會被歸因為產品品質問題,而非「偵測到竄改,服務依約定停止」的安全事件——恰好抹掉了本案要留的痕跡與法律敘事。
D11 拍板:改採 B 方案——lockdown 殼。gate(或 runtime 抽查/業務埋點)判定拒啟時,不直接 exit,改啟動一個 stdlib 極簡 HTTP responder 取代正常服務:
http.server)起,不掛 Flask、不接 DI container、不連 DB——零業務功能、零寫入面,能力面等於「回一頁靜態內容」。error_code = INTEGRITY_503001,data 帶 machine_fingerprint/tamper_event_id/detected_at 三欄——與正常 API error envelope 同形狀,FE 可用既有錯誤處理管線接。INTEGRITY_503001 即整站跳轉鎖定頁(不是個別 API 呼叫顯示錯誤 toast,是整站導頁——因為此時背後根本沒有真正的應用服務可用)。排除方案(詳見 §2 D11 決策列):A(純 exit)——即原始設計,體感等同故障,留不住法律敘事;C(半套:只寫 log 不起殼)——鎖定當下客戶端零可見資訊,喪失「當場出示 event_id」的即時證據呈現。
tamper 事件不再只記「哪個檔案 hash 不符」,擴充為完整存證 snapshot:
檔案層:每個不符檔案除預期/實際 hash 外,額外記錄:
mtime(修改時間)——最接近竄改行為發生時間點的證據,是四個欄位裡證據力最高的一個ctime(inode 變更時間,如 owner/權限被動過也會反映)owner(uid/gid,異常 owner 本身可能就是線索——例如竄改流程用了非預期身分寫入)size(檔案大小,佐證是否為整檔替換 vs 局部修改)觸發脈絡(依偵測來源分級 best-effort,不強求做不到的事):
| 偵測來源 | 蒐集內容 | 理由 |
|---|---|---|
| 業務埋點(HP,D8) | 當下登入帳號+來源 IP | 此時有完整 request context,是三者中證據最完整的情境 |
| runtime 抽查(SCH,D8/D10) | 活躍 session 快照(帳號/IP/最後活動時間) | 抽查非綁在某個 request 上,沒有「當下操作者」,退而求活躍 session 列表當旁證 |
| boot gate(BOOT,D3) | 僅檔案 metadata+app.log 尾段 |
此時服務尚未起、無 DB/無 session 可查,能拿到的只有檔案本身與上次運行留下的 log |
三個落點(FS 標記檔、DB integrity_tamper_events.detail、D9 回報 LC 的事件內容)同步吃這份擴充——detail JSONB 欄位結構見 §4.2,新增 file_metadata(mtime/ctime/owner/size per 檔)與 trigger_context(依來源分級的欄位,缺席即代表該來源拿不到,不硬填)兩個子物件。
必須向客戶與內部團隊明講的限制:本案偵測與存證的能力邊界止於「產品業務層」。實際竄改動作(替換 .so、改資料檔、動 manifest 之外的檔案)發生在主機層——SSH 登入後手動改、docker exec 進容器操作、或直接改 volume 掛載來源——這些管道完全不經過產品的任何業務邏輯,因此產品沒有、也不可能有「是誰做的」這個答案。D11 的 forensic 擴充蒐集的是「檔案證據」(mtime 等)與「若剛好有業務 session 在跑時的旁證」,不是身分溯源機制,兩者不可混淆。
契約措辭建議:條款不應寫成「查明竄改行為人即可求償」——這在應用層舉證上做不到,會讓契約條款落空。建議措辭方向是綁「產品完整性驗證失敗」這個客觀事實:
「產品完整性驗證失敗(即觸發本產品之防竄改鎖定機制)即構成違約事由,客戶應負相關責任,不以查明具體操作人員為前提。」
這個措辭下,產品證據鏈(manifest 簽章比對失敗、tamper_event_id、檔案 metadata、時間戳)本身就足以主張「完整性驗證失敗」這個事實,不需要額外證明「誰做的」——後者留給客戶內部(配合下方主機層審計建議)或司法程序視需要另行調查,不是本產品的責任範疇。
主機層審計是客戶責任範疇:產品能做的止於「偵測到+留下檔案證據」;要往上溯源到「哪個帳號、哪個 session 做的」,需要客戶主機層自己的審計機制(如 auditd 監控產物目錄的寫入事件)。落地版部署文件補一節具體建議,見 docs/features/FR-063-2608-nuitka-packaging/deployment-env.md §9(新增)。
file_metadata/trigger_context;發不出不重試不阻塞。main.py validate_required_env() 之後、create_app() 之前;api/socketio 兩模式共用(統一入口 main.py,RUN_MODE 分流前的共同路徑)。verify_payload(manifest 信封)(硬編公鑰)→ 逐檔比對 core+resources 兩層 → 任一失敗寫 FS 落點+D9 回報+exit。validate_required_env() 語氣:一次列全(驗章失敗 reason/不符檔案清單)、raise 終止。core/scheduler.py 啟動摘要字串與 main.py 檔頭模式裁剪表。root:root(chown 只留四個 volume 掛載點給 uid 1000),進程照舊 setpriv 降 1000 跑——檔案系統層寫不進產物。--read-only(+必要 tmpfs)建議。__pycache__ 疑慮);發現寫入需求→回報調整,不硬上。依賴鏈:.1 簽驗共用層是全部的前提 → .2 build manifest → .3 啟動 gate → .4 抽查/.5 unlock(可並行);.6 可全程平行;.7 收口。跨 repo(LC)項目已標注。
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-1.1 | LC 端抽 sign_payload(dict)→envelope 泛用層(payload 帶 type),sign_license 改走泛用層 |
LC license 簽發迴歸綠;新簽 license 檔與舊版驗證端互通 | — |
| T-1.2 | BE 端抽 verify_payload(envelope)→dict 泛用層,verify_license 改走泛用層 |
BE license 驗證迴歸綠;已出貨 license 檔(v2/v3)驗證通過(向後相容) | — |
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-2.1 | build_release.sh Step 4 尾端:dist 逐檔 SHA-256 → 分層 manifest(core/resources/thirdparty;④排除;manifest+sig 自身排除)→ 送 LC 簽章 → 兩檔入 dist | 一次完整 build 產出 manifest+sig;抽驗數檔 hash 正確;分層歸屬正確 | T-1.1 |
| T-2.2 | 排除規則與 build_image.sh rsync 排除對齊(單一真相:共用排除清單或斷言比對);manifest+sig 帶進 image context | image 內檔案集合與 manifest 清單 diff 為零(除自身兩檔) | T-2.1 |
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-3.1 | tamper 表 migration(比照 clock_watermark 形狀,含 GRANT)+FS 落點讀寫模組 | migration 套 DEV 過;FS 標記檔寫入/讀取/存在判定單元驗證 | — |
| T-3.2 | main.py 啟動 gate:FS tamper 檢查(存在即拒啟)→ verify_payload → ①②層逐檔比對 → 失敗寫 FS+exit;拒啟訊息含 fingerprint+event_id | 改一檔→啟動被拒且訊息完整;未改→正常啟動;api/socketio 兩模式皆生效 | T-1.2、T-2.1、T-3.1 |
| T-3.3 | FS→DB 紀錄補同步(服務起後首輪) | boot 期寫入的 FS 事件在服務起來後出現在 DB 表 | T-3.1、T-3.2 |
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-4.1 | APScheduler 抽查 job:4h+jitter 0–60min 每輪重抽;固定核心+隨機 N;D10 二次確認;偵測→雙落點+exit;同步啟動摘要字串與模式裁剪表 | 運行中改檔→下輪抽查(或縮短間隔測試)觸發 exit;兩模式都掛;單次 I/O 毛刺不誤鎖(模擬驗證) | T-3.2 |
| T-4.2 | 業務埋點 2–3 處(license 匯入/報告匯出等)順驗核心小集合 | 埋點路徑觸發驗證;核心檔被改時該操作即觸發 tamper;正常路徑延遲毫秒級 | T-3.2 |
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-5.1 | LC:unlock token 簽發功能(傾向後台頁,沿 62.10 internal API 慣例;形式實作時定)——輸入 fingerprint+event_id,產短效 token 檔 | 簽出的 token 檔產品端可驗;效期/nonce 欄位正確 | T-1.1 |
| T-5.2 | BE:token 核銷四關(驗章→fingerprint→event_id→nonce)+清 tamper 紀錄(FS+DB)+記 nonce | 正確 token→解鎖成功可重啟;錯 fingerprint/錯 event/重放 nonce/過期 各自被拒 | T-1.2、T-3.1 |
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-6.1 | 前置驗證:完整功能冒煙確認 runtime 無寫產物目錄行為(__pycache__ 疑慮);有寫入→回報調整 |
冒煙報告:產物目錄 mtime/新檔掃描零變動 | — |
| T-6.2 | entrypoint 改產物 root:root+部署文件補 --read-only 建議 | 容器內以 uid1000 嘗試改產物→Permission denied;服務正常 | T-6.1 |
| T-6.3 | LC:tamper 事件接收端點(62.10 internal API 形狀);BE:偵測時 best-effort 回報(不重試不阻塞) | 有網→事件現於 LC;斷網→鎖定流程不受影響、無阻塞 | T-3.2(BE 側) |
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-7.1 | 母案卡五條驗收全鏈實測(見 §6)+License 既有流程迴歸 | §6 五條全過;license 匯入/驗證迴歸綠 | .1–.6 全部 |
D11 為 2026-08-16 補充定案,拆四張子卡;T-8.1/T-8.3 涉及既有 gate/落點程式碼,需等主鏈(.1–.6)程式面完成後開工,T-8.2(FE)契約已定可先行,T-8.4(本段文件)為前置。
| # | 子任務 | 驗收 | 依賴 |
|---|---|---|---|
| T-8.1 | lockdown 殼實作:偵測拒啟時起 stdlib HTTP responder 取代 exit(無 Flask/DI/DB),固定回鎖定 HTML+JSON(INTEGRITY_503001+三欄 data) |
觸發拒啟後打任意路徑皆回鎖定頁;殼程序本身無 DB/DI 依賴(cold 環境也能起);unlock 核銷流程不受影響(仍走下次啟動正常 boot) | CM-1224/1225/1228(主鏈 gate/落點程式碼完成) |
| T-8.2 | FE 認 INTEGRITY_503001 整站導向鎖定頁 |
任一 API 回該 error_code → 整站跳轉;鎖定頁顯示 fingerprint/event_id | 契約已定,可先行 |
| T-8.3 | forensic 擴充:detail 補 file_metadata(mtime/ctime/owner/size)+trigger_context(依來源分級);三落點(FS/DB/D9 回報)同步吃擴充 |
觸發不符時 DB detail 含新欄位;boot/runtime/業務埋點三種來源各自產出對應分級內容 |
CM-1224/1225/1228(落點模組完成) |
| T-8.4 | 本卡(CM-1234):design.md §4.4/§2/§5 文件落地+部署文件補主機層審計建議節 | 本段文件與拍板內容一致;deployment-env.md 新增節可讀 | — |
母案卡(CM-1188)五條驗收條件,展開為可執行步驟:
1. 改檔拒啟:以出貨 image 起容器確認正常服務 → 停容器 → 修改①層任一 .so(或②層任一資料檔)一個 byte → 重啟 → 預期:啟動被拒、log 列出不符檔案與 reason、顯示 machine_fingerprint+tamper_event_id、FS 落點出現標記檔。
2. 運行中改檔觸發:正常運行中修改固定核心清單內一檔 →(測試時縮短抽查間隔或觸發業務埋點路徑)→ 預期:D10 複算兩次皆不符 → tamper 雙落點寫入 → 服務立即 exit;有網情境 LC 端出現該事件(D9)。
3. 重啟被拒(鎖定持續):承 2,直接重啟容器 → 預期:不進入驗證流程即拒啟(tamper 紀錄存在=視同無 license);再測「容器重建但 volume 保留」→ 仍拒啟(FS 落點生效)。
4. unlock 恢復:LC 以該機 fingerprint+event_id 簽發 unlock token → 產品端核銷 → 預期:tamper 紀錄清除、nonce 記錄、重啟恢復正常服務;負面案:同 token 重放被拒、他機 token 被拒、過期 token 被拒。
5. 正常無誤報:未竄改的出貨 image 連續運行 ≥24h(跨多輪抽查)+完整功能冒煙(含④volume 大量寫入:上傳檔案、log 滾動)→ 預期:零 tamper 事件、零誤鎖;啟動時間增量在秒級預算內。
另加迴歸底線:License 既有簽發/匯入/驗證全鏈(含已出貨舊 license 檔)不受 D2 抽層影響。