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。
本案是「三階段落地版」戰線的第二棒(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 解決「原始碼不外流」(客戶拿到的是機器碼),本案解決「機器碼被動手腳怎麼辦」——兩者合起來才構成落地版的完整保護面。
偵測程式被竄改 → 終結服務並鎖定產品;唯一解鎖 = 原廠簽發的一次性 unlock token。
拆開是四個動詞:
build 期產生逐檔 SHA-256 manifest,以原廠私鑰簽章;產品端啟動時驗章+逐檔比對,runtime 由 APScheduler 低頻抽查(隨機 N+固定核心清單)。檢查點分散多處、互相驗證,拉高「全拔乾淨」的成本。
偵測到竄改,立即 exit——不降級、不告警後繼續跑。半殘狀態繼續服務比停機更危險(資料正確性無法保證,且等於容忍竄改)。
tamper 事件雙落點(DB+檔案系統)。之後每次啟動先查 tamper 紀錄,存在即視同無 license 拒啟——把「重啟就好」這條路封掉。
原廠簽發一次性 unlock token(LC 私鑰簽的離線信封:machine_fingerprint+nonce+效期),產品端離線驗章+本地記已用 nonce 防重放。離線環境必須成立——被鎖的機器可能根本連不到原廠。
這不是 DRM 軍備競賽,是「拉高成本+留下痕跡」
client-side 完整性驗證無法對抗有決心的逆向團隊——驗證邏輯本身就在客戶手上的機器碼裡,理論上總是拆得掉。本案的目標定位是:
以下事實皆經實碼/實測驗證(FR-062 LC 與 BE 簽驗實碼、FR-063 build 管線與產物),是後面所有決策項的基礎。
| 元件 | 座標 | 現況 |
|---|---|---|
| 簽章演算法 | 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 正是該階段。
缺口一:簽驗函式型別綁死 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。
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 唯讀化)的直接動機:驗證是「事後偵測」,唯讀化是「事前擋掉」,兩者互補。
啟動序(main.py):load_dotenv → RUN_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。
scheduler(core/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 檔頭模式裁剪表。
| 面向 | 可復用 | 缺口 |
|---|---|---|
| 簽章/驗章 | 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) |
全鏈: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: 下次啟動恢復正常驗證流程
%%{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
共用一把鑰匙、一條 code path:圖中 sign_payload / verify_payload 是 D2 要抽的泛用層——license、manifest、unlock token 三種 payload 走同一簽驗實作、同一把公鑰。拆掉驗證的人等於同時拆掉 license,「拆一個等於拆兩個」。
以下為草案形狀,實作前隨 D1–D8 拍板調整。
// 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
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 仍在,兩者任一存在即拒啟。
{
"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 紀錄。
子需求雛形(實際開卡與順序待 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+實機 | 全部 |
D1 保護範圍分層——啟動全驗①②,③第三方只走 runtime 抽查?
產物四層(§2.3):①業務(數百檔)②隨版資料(數百~千餘檔)③第三方(2–4 萬檔)④volume(必排除)。方案:
建議:採 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 年就是實質關閉)。要調參數走發版。