FR-064 · Tamper Detection — 需求討論稿 · 2026-08-15(v1 待審)

程式被動過,就終結服務、鎖定產品

三階段落地版戰線第二棒:FR-063 已把整套 BE 編成機器碼出貨(Harbor guidant-ai-be:1.14.0),本案接著回答「產物被竄改怎麼辦」——build 期逐檔 SHA-256 manifest 以原廠私鑰簽章、啟動驗章+逐檔比對、runtime 低頻抽查;一旦偵測竄改,寫入 tamper 紀錄(DB+檔案系統雙落點)並立即終結,之後每次啟動視同無 license 拒啟。唯一解鎖途徑=原廠簽發的一次性 unlock token(離線信封,air-gapped 環境也成立)。簽驗體系復用 FR-062 License Center,不另建 PKI。

待決策 D1–D8(8 項) 前作:FR-063(Nuitka 編譯,已完結) 後續:FR-065(Installer) 復用:FR-062 LC 簽驗體系 Notion 母案 CM-1188
§1

需求分析:這一棒要守什麼、承認守不住什麼

1.1 戰線位置

本案是「三階段落地版」戰線的第二棒(Notion 母案 CM-1188):

棒次 案號 內容 狀態
第一棒 FR-063 Nuitka 編譯——整套 BE 編成機器碼出貨,產物 Harbor guidant-ai-be:1.14.0(bake cc7714d2 已完結
第二棒 FR-064 防竄改偵測(本案)——偵測產物被竄改 → 終結服務並鎖定產品;唯一解鎖=原廠一次性 unlock token 討論中
第三棒 FR-065 Installer——客戶端安裝/升級體驗 未開

FR-063 解決「原始碼不外流」(客戶拿到的是機器碼),本案解決「機器碼被動手腳怎麼辦」——兩者合起來才構成落地版的完整保護面。

1.2 需求一句話

偵測程式被竄改 → 終結服務並鎖定產品;唯一解鎖 = 原廠簽發的一次性 unlock token。

拆開是四個動詞:

① 偵測

build 期產生逐檔 SHA-256 manifest,以原廠私鑰簽章;產品端啟動時驗章+逐檔比對,runtime 由 APScheduler 低頻抽查(隨機 N+固定核心清單)。檢查點分散多處、互相驗證,拉高「全拔乾淨」的成本。

② 終結

偵測到竄改,立即 exit——不降級、不告警後繼續跑。半殘狀態繼續服務比停機更危險(資料正確性無法保證,且等於容忍竄改)。

③ 鎖定

tamper 事件雙落點(DB+檔案系統)。之後每次啟動先查 tamper 紀錄,存在即視同無 license 拒啟——把「重啟就好」這條路封掉。

④ 解鎖

原廠簽發一次性 unlock token(LC 私鑰簽的離線信封:machine_fingerprint+nonce+效期),產品端離線驗章+本地記已用 nonce 防重放。離線環境必須成立——被鎖的機器可能根本連不到原廠。

1.3 定位與已知限制(已與決策者對齊)

這不是 DRM 軍備競賽,是「拉高成本+留下痕跡」

client-side 完整性驗證無法對抗有決心的逆向團隊——驗證邏輯本身就在客戶手上的機器碼裡,理論上總是拆得掉。本案的目標定位是:

  • 99% 的人覺得不值得:拆解需要同時處理 binary 內的驗證邏輯、分散的檢查點、簽章公鑰——成本遠超「試試看改兩個檔」的程度。
  • 1% 蓄意者留下痕跡:tamper 紀錄、被替換的檔案本身,都是合約求償的證據。本產品是 B2B 具名法人場景——客戶是簽了約的公司,不是匿名散戶;法律層才是真實防線,技術層負責讓違約「有痕跡、可舉證」。
  • 離線環境必須成立:客戶環境可能完全 air-gapped,沒有「打回原廠回報」這條線。所有偵測、鎖定、解鎖都必須在無網路下自洽。
§2

現況盤點:可復用 vs 缺口

以下事實皆經實碼/實測驗證(FR-062 LC 與 BE 簽驗實碼、FR-063 build 管線與產物),是後面所有決策項的基礎。

2.1 FR-062 簽驗體系(本案的復用底座)

元件 座標 現況
簽章演算法 Ed25519,簽 canonical JSON bytes(sort_keys +緊湊分隔) LC 端 src/license_center/core/crypto/engine.py:56 sign_license();BE 端有逐位元組一致的 canonical 實作 common/license/engine.py:90
kid 公鑰 SHA-256 hex 前 16 chars keys.py:21 compute_kid()
私鑰 PKCS8 PEM + passphrase(env LICENSE_CENTER_KEY_PASSPHRASE;air-gapped 回退表單輸入) LC 端持有,產品端永不落地
信封格式 v2 信封(kid / payload zlib+b64 / signature)+ v3 armor(PEM 風格) 兩端已互通
產品端公鑰 common/license/public_keys.py 硬編源碼(現三把 DEV/STG/POC) 隨 binary 編譯,改公鑰=改 binary
驗章核心 common/license/engine.py:138 verify_license() 純函式
機器指紋 common/license/machine_fingerprint.py compute_machine_fingerprint() 現成,unlock token 直接用
一次性碼借鏡 LC activation_code(一次性、activated_at 判已用、重用 409)三件套 模式可抄,但它是線上模型——unlock 要改離線信封(見 D6)

執法慣例(license 側已定型、本案沿用同語氣):fail-closed;總開關 LICENSE_ENFORCEMENT_ENABLED;驗失敗 log 留證(content_fingerprint+reason)。當時「表格化留證」被註記為「留給後續階段」——FR-064 正是該階段

2.2 缺口(要動的地方)

缺口一:簽驗函式型別綁死 License 商務欄位

sign_license() / verify_license() 目前綁死 License 的 18 個商務 key。要做到母案卡要求的「與 license 驗證共用同一 code path、同一把公鑰」,必須抽出泛用層 sign_payload(dict) → envelope / verify_payload(envelope) → dict,License 與 manifest(以及 unlock token)都改走它——LC 與 BE 兩端都要動。詳見 D2。

缺口二:啟動期驗證掛點目前不存在

license 驗章只在「照匯入」時觸發;create_app()沒有任何啟動驗章。且 verify_license 依賴 DB 的 clock_watermark——manifest 驗章必須走無 DB 依賴的路徑(啟動時 DB 可能還沒 ready,且驗證本身不該依賴可被客戶操作的儲存)。詳見 D3。

缺口三:tamper 是機器級,現有 license 表形狀不合

tamper 事件是機器級/安裝級,不是租戶級。DB 落點語意最接近 public.license_clock_watermark(全域單例、不掛 RLS),不適合套 tenant_licenses 那種掛 RLS 的形狀。要開新表。詳見 D5。

2.3 FR-063 產物與 build 管線(manifest 的對象)

build 管線scripts/build/build_release.sh 四步——deps → resources(.mo)→ freeze(DI 靜態清單+version bake)→ compile(Nuitka → dist + excluded 套件複製 + assert_protection 零 .py +資料檔存在斷言)。manifest 產生的建議插入點:Step 4 尾端、全部斷言通過之後、進 build_image 之前——那是 dist 定案的時刻。

🔴 build_image 的 context 是 rsync 後另一份

build_image.sh 用 rsync 複製出另一份 context(排除 .env* / log / static / smoke-*.log / __pycache__)。manifest 的排除規則必須與 rsync 排除規則一致,否則簽了進不了 image 的檔、或 image 裡有沒簽的檔,啟動驗證必對不上。manifest +簽章檔要一併帶進 context。

產物分四層(這個分層直接決定 D1 的驗證分層):

內容 規模 竄改攻擊價值
① 業務資產 binary 本體+Nuitka .so(零 .py) 數百檔 最高——業務邏輯、授權執法都在這
② 隨版資料檔 translations .mo、ssp_cmmc_template.docx、cmmc canon json ×2、resources/download 5 檔、project_summary_report templates html 數百~千餘檔 中——改資料檔可影響產出正確性
③ 第三方原樣附帶 EXCLUDE_COMPILE_PKGS 11 支(pymupdf/fitz、pandas、numpy、fontTools、pdfminer、Crypto、PIL、grpc、lxml、rapidfuzz)+相依封閉集合+dist-info+.libs 2–4 萬檔量級,.py 佔大宗 低——但 .py 可讀可改,是「最容易下手」的面
④ volume /app/static /app/log /home/guidant /opt/guidant/pki /tmp 動態 必排除——客戶正常寫入,納入即誤報

全 dist 啟動逐檔掃描估數秒~十數秒 I/O——這是 D1 分層驗證的動機。

🔴 威脅前提:「產物唯讀」目前是意圖,不是事實

Dockerfile COPY --chown=1000:1000,entrypoint 以 root 起、只 chown 四個掛載點後 setpriv 降 1000 跑 binary——/app 的 owner 就是執行者本人,非 root 進程對產物可寫。竄改門檻目前是零。這是 D4(/app 唯讀化)的直接動機:驗證是「事後偵測」,唯讀化是「事前擋掉」,兩者互補。

2.4 啟動序與 scheduler(掛點)

啟動序main.py):load_dotenvRUN_MODE 解析(SystemExit fail-fast)→(socketio: monkey_patch)→ validate_required_env()(缺項一次列全 raise ConfigError)→ create_app()REGISTERED_APPS 逐一 import(RuntimeError fail-fast)。

manifest 驗證最自然的位置:validate_required_env() 之後、create_app() 之前——同層級、同 fail-fast 語氣、不依賴 DB/DI。

schedulercore/scheduler.py init_scheduler):api 模式現有 4 jobs(drive_sync 5s/webhook_renewer 6h/framework_cleanup cron/license_expiry cron),既有 pattern=app_context → DI container → session_scope。完整性抽查 job 不需 DB/DI,可省 container。新增 job 要同步 L173 啟動摘要字串與 main.py 檔頭模式裁剪表。

2.5 可復用 vs 缺口總表

面向 可復用 缺口
簽章/驗章 Ed25519 canonical JSON、kid、v2/v3 信封、verify_license 純函式骨架 型別綁死 License 18 key → 抽 sign_payload/verify_payload 泛用層(D2)
公鑰配送 public_keys.py 硬編源碼慣例(隨 binary 編譯) 無——直接沿用(D3)
機器指紋 compute_machine_fingerprint() 現成
一次性碼 LC activation_code 三件套模式 線上模型 → 改離線簽章信封(D6)
執法語氣 fail-closed、log 留證慣例 啟動掛點不存在;tamper 表不存在(D3、D5)
build 管線 build_release.sh 四步結構、斷言機制 manifest 產生步驟;排除規則與 rsync 對齊
scheduler init_scheduler 掛 job 機制 抽查 job 本體(D8)
檔案系統防線 entrypoint chown 機制已存在 產物 owner=執行者,需唯讀化(D4)
§3

端到端流程

全鏈:build 簽章 → 啟動驗證 → runtime 抽查 → tamper 鎖定 → 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->>APP: 立即 exit
        else 全部通過
            APP->>APP: create_app() → 正常服務
        end
    end

    Note over SCH,ST: Runtime(低頻抽查)
    loop 每 45min+jitter(D8)
        SCH->>SCH: 隨機 N(③第三方)+固定核心(①②全量)重算 hash
        alt 單檔不符
            SCH->>ST: 寫入 tamper 事件(雙落點)
            SCH->>APP: 立即 exit(終結服務)
        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+查本地已用 nonce
    APP->>ST: 清除 tamper 紀錄+記錄 nonce 已用(防重放)
    APP->>APP: 下次啟動恢復正常驗證流程
圖 1 — 防竄改端到端時序(build → 啟動 → runtime → 鎖定 → 解鎖)
§4

架構:LC 簽發端 vs 產品端

%%{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 簽發<br/>CLI 或後台頁(D6)"]
      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/>隨機 N+固定核心<br/>api/socketio 兩模式都跑"]
    TS["tamper 落點(D5)<br/>DB 全域單例表+FS volume 檔"]
    RO["/app 唯讀化(D4)<br/>chown root:root+--read-only"]
    UV["unlock 驗證(D6)<br/>fingerprint 比對+nonce 防重放"]
    BOOT --> VP
    RT --> VP
    BOOT -->|"偵測竄改"| TS
    RT -->|"偵測竄改"| TS
    TS -->|"存在即拒啟"| BOOT
    UV -->|"清除紀錄"| TS
    UV --> VP
  end

  IMG -.->|"image 出貨"| CLIENT
  UT -.->|"token 檔離線交付"| UV
圖 2 — 元件架構:原廠側(LC+build)與客戶側(產品 binary)

共用一把鑰匙、一條 code path:圖中 sign_payload / verify_payload 是 D2 要抽的泛用層——license、manifest、unlock token 三種 payload 走同一簽驗實作、同一把公鑰。拆掉驗證的人等於同時拆掉 license,「拆一個等於拆兩個」。

§5

資料模型草案

以下為草案形狀,實作前隨 D1–D8 拍板調整。

5.1 manifest 格式(dist 內兩個檔)

// 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": "..."}}
  }
}
  • 信封本身照 FR-062 v2/v3 格式(kid+zlib+b64 payload+signature),另存一檔(如 integrity-manifest.sig)。
  • manifest 與簽章檔自身不入清單(驗不了自己,見 D7);④ volume 路徑一律排除。
  • layers 分層是為了 D1:啟動 gate 只吃 coreresources,抽查 job 吃全部。

5.2 tamper 落點

DB:新全域單例語意表(比照 license_clock_watermark 形狀——public schema、不掛 RLScm_app 可寫):

-- 草案,非最終 DDL
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
    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/ 下,D5 拍板),內容含 event_uid+detected_at+detail 摘要——DB 被還原時 FS 仍在,FS 被刪時 DB 仍在,兩者任一存在即拒啟

5.3 unlock token payload

{
  "type": "unlock_token",
  "machine_fingerprint": "…",     // 必須與被鎖機器一致
  "tamper_event_id": "event_uid", // 綁定特定事件
  "nonce": "隨機 64 hex",          // 一次性,用過即記錄
  "issued_at": "…",
  "expires_at": "…"               // 短效期(如 72h)
}

同樣走 v2/v3 信封+LC 私鑰簽章;產品端 verify_payload 驗章 → 比對 fingerprint → 查本地已用 nonce → 三關全過才清除 tamper 紀錄。

§6

階段拆分草案

子需求雛形(實際開卡與順序待 D1–D8 拍板後定案):

子需求 內容 Repo 依賴
FR-064.1 簽驗共用層 sign_payload/verify_payload 泛用層(payload 帶 type);License 既有簽發/驗證改走泛用層+迴歸驗證 LC+BE 無(是其他全部的前提,D2)
FR-064.2 build 期 manifest build_release.sh Step 4 尾端產 manifest+送簽;排除規則與 rsync 對齊;manifest+信封入 image context BE(build scripts)+LC 064.1
FR-064.3 啟動驗證 gate main.py env 驗證後掛驗章+①②層逐檔比對;tamper 紀錄檢查(存在即拒啟);tamper 表 migration+FS 落點 BE 064.1、064.2
FR-064.4 runtime 抽查 APScheduler job(隨機 N+固定核心);api/socketio 兩模式;偵測即雙落點+exit BE 064.3
FR-064.5 unlock 流程 LC 端簽發功能(CLI 或後台頁,D6);產品端驗 token+防重放+清紀錄 LC+BE 064.1、064.3
FR-064.6 /app 唯讀化 entrypoint chown root:root /app;驗證 runtime 無寫產物行為;部署文件補 --read-only 建議 BE(Docker) 可平行(D4)
FR-064.7 E2E 驗收 竄改一檔→啟動拒啟/runtime 抽中→exit→拒啟→unlock 復活 全鏈實測;License 既有流程迴歸 test repo+實機 全部
§7

待決策 D1–D8

D1 保護範圍分層——啟動全驗①②,③第三方只走 runtime 抽查?

產物四層(§2.3):①業務(數百檔)②隨版資料(數百~千餘檔)③第三方(2–4 萬檔)④volume(必排除)。方案:

  • A(建議)分層:啟動 gate 全驗①②(秒級可接受);③全部納 manifest 但只由 runtime 抽查覆蓋。
  • B 全量啟動驗:最嚴,但啟動多花十數秒 I/O,且每次重啟都付。
  • C 只驗業務①:最快,但資料檔(模板/canon json)被改會影響產出正確性而完全無感。

建議:採 A。全驗第三方是把啟動拖慢十數秒換不到等值防護——第三方 .py 被竄改的攻擊價值低於業務碼,交給抽查長期覆蓋即可。

D2 簽驗共用層抽象——抽 sign_payload / verify_payload 泛用層?

現況 sign_license()/verify_license() 型別綁死 License 18 個商務 key(§2.2 缺口一)。抽泛用層 sign_payload(dict)→envelope / verify_payload(envelope)→dict,payload 帶 type 欄位區分 license / manifest / unlock_token;LC 與 BE 兩端同步改,License 既有路徑做迴歸驗證。

風險:動 LC 端屬跨 repo 異動;License 既有簽發/驗證需完整迴歸。

建議:必做——這是「共用同一 code path 同一把公鑰」的實作前提,也是「拆一個等於拆兩個」防護敘事的基礎。不抽就是第二套簽驗實作,違反不重複造輪子鐵則。

D3 啟動驗證錨點——main.py env 驗證後、create_app 前?

驗證放 validate_required_env() 之後、create_app() 之前(§2.4):同層級、同 fail-fast 語氣、無 DB 依賴(有別於 verify_license 要 clock_watermark)。公鑰沿用 public_keys.py 硬編慣例、烤進 binary。

建議:採此錨點。技術上可以更早(load_dotenv 前),但更早驗不到什麼多的東西、還要放棄 env 設定能力,收益低。

D4 /app 唯讀化——entrypoint 改 chown root:root?

現況 /app owner=執行者(uid 1000),非 root 進程對產物可寫(§2.3 威脅前提)。方案:entrypoint 改 chown root:root /app(產物歸 root、進程跑 1000 → 檔案系統層寫不了),部署文件加 --read-only 建議。

前置驗證:需確認 Nuitka binary 與附帶套件 runtime 無寫產物目錄的行為——③層有 .py,若有直譯器附帶場景要確認 __pycache__ 寫入;有寫入需求則此案要調整。

建議:做——最便宜的第一道防線,與驗證互補(唯讀化擋事前、驗證抓事後)。先跑一輪完整功能冒煙確認無寫入行為再上。

D5 tamper 落點與抹除成本——雙落點;「可洗掉」接不接受?

DB=新全域單例表(比照 license_clock_watermark:不掛 RLS、cm_app 可寫,§5.2);FS 落點候選 /opt/guidant/pki(已是持久掛載、語意相近)——是否再藏第二份 FS 落點待議。

已知限制:容器重建+DB 還原可以洗掉全部紀錄。在「拉高成本+留痕跡」定位下這是否接受?或加碼(如 tamper 時把事件指紋織進下次 license 驗證流程,讓「洗紀錄」也留下可比對的斷點)?

建議:雙落點照母案卡做,接受「可洗掉」的邊界並明文寫入已知限制——會做到「重建容器+還原 DB」的人已屬蓄意,蓄意本身在 B2B 合約場景就是求償事由;加碼方案留作後續強化選項不進本案。

D6 unlock token 離線信封——LC 端簽發介面做 CLI 還是後台頁?

token 形狀已定(§5.3):LC 簽 {type:unlock, machine_fingerprint, tamper_event_id, nonce, expires_at},產品端離線驗章+比對 fingerprint+本地已用 nonce 防重放。LC activation_code 的線上三件套模式不適用(被鎖機器可能連不到 LC),故改離線信封。

待選:LC 端簽發功能的形式——CLI 就好(原廠內部人員操作,量極少)vs 後台頁(LC 已有後台,加一頁)。

建議:採離線信封;簽發介面 CLI 起步即可(tamper 解鎖是極低頻、原廠內部操作,後台頁等有實際頻率再補)。

D7 驗證邊界承認——binary 外的東西不驗?

manifest+簽章檔放 dist 內、自身不入清單(驗不了自己);entrypoint、Dockerfile、image 層不在 client 端驗證範圍——binary 外的東西沒有可信錨點去驗。防線改由三件事承擔:D4 唯讀化+驗證邏輯本身是機器碼+檢查點分散互驗。

建議:承認邊界並明文寫入已知限制。image 層簽章驗證(cosign/digest pinning)屬 FR-065 installer 的範圍,不在本案硬撐。

D8 抽查參數——寫死還是可設定?

預設建議:interval 45min+jitter;每輪隨機 N=200(③第三方層)+固定核心清單(binary 本體+全部業務 .so+資料檔,即①②全量);api/socketio 兩模式都跑(socketio 也是攻擊面);判定=單檔 hash 不符即觸發 tamper(不設寬容度)。

待選:參數全部可 env 覆寫,還是寫死在 binary?

建議:寫死——「可設定」等於「可關閉」(env 是客戶可控面,interval 設成 10 年就是實質關閉)。要調參數走發版。