FR-167 · 技術規格 · 2026-11-01

發布簽章

發布清單的 v2 信封、canonical 與 Ed25519、kid、印章一正一備與換鑰、guidant-release CLI 全部子命令、主系統與主機的驗證順序、測試章與正式章現況、威脅對照

客戶主機要不要裝一個更新包,只看一件事:這份發布清單是不是用「客戶機上已經裝好的印樣」驗得過。私鑰只在決策者電腦,更新站與 CI 都沒有;客戶端不信任更新站、上傳者或包裡帶來的任何鑰。本頁以 update-center cli/ 與主系統 package_verifier.py、release_keys.sh 的程式碼為準。

§1

信任鏈一覽

角色 手上有什麼 能做什麼 不能做什麼
決策者電腦 發布印章私鑰(PKCS8 加密 PEM,passphrase 只在腦中) guidant-release publish 蓋章 —
GitLab CI register-draft 專用 CI token、R2 上傳金鑰 登記草稿、上傳包與未簽清單 發布、撤回、簽任何東西
更新站 LC 公鑰表、R2 預簽用金鑰 認客戶、選版、原樣轉交簽好的清單 簽清單、改清單(改了客戶驗不過)
客戶主系統(BE) 本機 /etc/guidant/release-keys/<kid>.pub(容器內唯讀掛載) 驗清單、驗包、寫更新單 信任更新站回的雜湊或鑰
客戶主機(shell) 同一個資料夾的印樣、openssl、python3 或 perl 升級第 0 步與預檢再驗一次 用包裡帶來的鑰驗包裡的清單
%%{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
  autonumber
  participant CI as GitLab CI
  participant US as 更新站
  participant R2 as R2
  participant DM as 決策者 CLI
  participant BE as 客戶主系統
  participant H as 客戶主機
  CI->>US: register-draft(未簽 release.json 的欄位)
  US-->>CI: upload.bundle/sha256/manifest 三個 key
  CI->>R2: 上傳整包、.sha256、未簽 release.json
  DM->>US: GET /admin/releases/<v>(Access)
  US-->>DM: 詳情(version、sha256、size、min_base…、內部先裝狀態)
  Note over DM: 核欄位合理、內部先裝全成功<br/>輸入版號確認、passphrase 解鎖<br/>buildPayload → canonical → Ed25519<br/>用 .pub 自驗
  DM->>US: POST …/publish {signed_manifest, kid}
  BE->>US: POST /v1/check
  US-->>BE: releases[].signed_manifest(原樣)
  Note over BE: 本機印樣驗章 → 版號遞增 → 主版號 → 可直升
  BE->>US: POST /v1/download-url
  US-->>BE: 15 分鐘預簽網址
  BE->>R2: 下載整包
  Note over BE: 大小與 sha256 等於驗過章的清單
  BE->>H: 寫更新單(包+清單放 incoming/)
  Note over H: 第 0 步與預檢:release_keys_canonical 再驗章、<br/>sha256 三方相符
圖 1 — 一版從出包到客戶裝上的簽章路徑
§2

信封 v2 格式

格式與 LC core/crypto/engine.py 的 sign_payload 同一套 v2 信封(cli/envelope.ts:1-10):

{"alg":"Ed25519","format_version":2,"kid":"<主簽 kid>","payload":"<base64(zlib(canonical))>","signature":"<base64>",
 "cosignatures":[{"alg":"Ed25519","kid":"<新章 kid>","signature":"<base64>"}]}
欄位 型別 必填 說明
format_version number 是 固定 2
alg string 是 固定 "Ed25519"
kid string 是 主簽章的 kid;必須等於 payload 內的 kid
payload string 是 base64(zlib.deflate(canonical))
signature string 是 base64(Ed25519(canonical))——簽的是 canonical 原文,不是壓縮後的字串
cosignatures [{kid, alg, signature}] 否 只在換鑰中出現;新章對同一份 canonical 再簽一次

序列化:信封本身也用 canonical JSON 輸出(serializeEnvelope,鍵排序、無空白,cli/envelope.ts:93-95)。之後更新站與客戶端都把它當字串原樣傳遞,不 parse 再 stringify,免得改到一個位元組。更新站存在 product_releases.signed_manifest,主機上存在包旁 guidant-ai-<版>.release.json。

大小上限:信封與解壓後的 canonical 各 64KB(更新站 MAX_SIGNED_MANIFEST;BE MAX_ENVELOPE_BYTES、MAX_CANONICAL_BYTES,package_verifier.py:29-30)。BE 解壓用 decompressobj 帶上限,超過就丟(解壓炸彈)。

§3

payload 與 canonical

buildPayload(cli/envelope.ts:53-74)只挑契約欄位;更新站回的詳情還有 status、r2_key 等,那些不蓋進章。

欄位 型別 必有 來源 說明
payload_type string 是 固定 "release_manifest"。同一套信封也用在授權檔、完整性 manifest;另立型別防止互相頂替
version string 是 更新站詳情
major number 是 更新站詳情 CLI 核過等於版號第一段
min_base_version string 是 更新站詳情
sha256 string 是 更新站詳情 整包雜湊,64 hex
size number 是 更新站詳情 整包位元組數
has_security_fix boolean 是 更新站詳情
issued_at string 是 CLI 當下時間 2026-10-29T08:00:00+00:00 格式:秒、+00:00、不帶毫秒(isoSeconds,cli/commands.ts:46-48),Python 3.10 以前的 fromisoformat 不收 Z
kid string 是 主力章
min_agent_version string 有值才放 更新站詳情 沒值時不放這個鍵(不放 null),舊清單 canonical 不變
ops_sha256、ops_size string、number 兩個都有值才放 呼叫端傳入 維運指令小檔的雜湊與大小;缺一就兩個都不放
endorsed_keys [{kid, pubkey}] 換鑰中才放 新章 pubkey 是 base64 的 32 bytes 公鑰

canonical = payload 的鍵排序、無空白 JSON,與 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False) 逐位元組一致(canonicalJson,src/licenses.ts:70-78,CLI 直接 import 同一支)。陷阱:JS 與 Python 對浮點數輸出不同(1.0 vs 1),payload 裡沒有浮點欄位,所以一致;新加欄位時不可加浮點數。

ops 兩欄的現況限制:CI 產的未簽 release.json 有 ops_sha256/ops_size(scripts/build/release_json.sh:11、:155),但 register-draft 不收這兩欄、D1 沒有對應欄位、更新站詳情也不回,所以正式 CLI publish 路徑簽出來的清單沒有 ops 兩欄。客戶主機遇到沒有 ops 欄位的清單會退回從整包抽維運指令(較慢、功能正常,self_replace.sh sr_ops_verified)。測試期發布是直接用測試章對 188 產出的 release.json 簽,所以帶得到這兩欄。

演算法:Ed25519(Node crypto.sign(null, canonical, key)),簽 canonical 的 UTF-8 bytes。

kid = 公鑰原始 32 bytes 的 sha256 前 16 個小寫 hex(computeKid,cli/keystore.ts:42-44),與 LC 同一套規則。可以用 openssl 從 .pub 算:

openssl pkey -pubin -in <kid>.pub -outform DER | tail -c 32 | shasum -a 256 | cut -c1-16
§4

印章(金鑰)

本機保存

項目 規格 來源
資料夾 預設 ~/.guidant-release,--keys 可換;建立時 700 cli/keystore.ts:29-31、:66-69
角色表 keys.json:{keys:[{kid, role, created_at, revoked_at?, endorsed_in?}]},600,先寫暫存再 rename :58-64
角色 primary(主力)、backup(備用)、next(換鑰中的新章)、revoked(作廢) :15
私鑰檔 <kid>.key:PKCS8 PEM,passphrase 加密(Node 產出為 PBES2+AES-256-CBC),600;flag: "wx",同名檔已存在就失敗,絕不覆蓋 :71-81
公鑰檔 <kid>.pub:SPKI PEM,644。客戶主機用 openssl pkeyutl -verify -pubin -inkey <kid>.pub 直接吃 :79
passphrase 至少 12 bytes;只從 TTY 讀(stdin 不是終端機就拒絕),不收參數、不讀環境變數;關 echo 逐位元組收,連 * 都不印;回 Buffer 用完 fill(0) :26、cli/prompt.ts
解鎖檢查 私鑰檔權限比 600 寬就拒絕;passphrase 錯或檔壞給白話錯誤;解開後再算一次 kid,與檔名不符就拒絕(檔名被改過) :83-99

一正一備

init-keys 一次產兩把:主力章與備用章,兩把 passphrase 必須不同(同一個 passphrase 外流時備用會一起失守,cli/commands.ts:89-92)。產完印出封存說明:

  1. 備用章私鑰複製到兩個離線隨身碟,分兩地保管。
  2. 備用章 passphrase 手寫密封,與隨身碟分開放,不存在任何電腦、手機、雲端、聊天紀錄。
  3. 兩份都確認讀得到後,刪掉這台電腦上的備用章私鑰(.pub 留著)。
  4. 兩個 .pub 都交給主產品放進 scripts/installer/release-keys/。

客戶機同時認兩把印樣,所以主力章出事時 promote-backup 立刻生效、不用背書。

公鑰進產品

位置 內容 誰寫
BE repo scripts/installer/release-keys/<kid>.pub 隨出貨包的印樣;PROD_KIDS 一行一個正式章 kid 人工交付(決策者給 .pub)
出貨包 guidant-ai-<版>/release-keys/ 同上,打包時帶入;ops 小檔也帶 build_bundle.sh
客戶主機 /etc/guidant/release-keys/<kid>.pub root 755/檔 644 release_keys_install(scripts/installer/lib/release_keys.sh:99-127),升級第 0 步換指令時呼叫(self_replace.sh:180)
主系統容器 /etc/guidant/release-keys(唯讀掛載) 同上 docker/production/docker-compose.yml:257;BE 設定 GUIDANT_RELEASE_KEYS_DIR(預設該路徑,config/config.py:209)

release_keys_install 的規則:

  • 只加不減:同名不覆蓋(kid 是公鑰指紋,同名等於同鑰)、不刪舊的——舊客戶機要靠舊印樣驗下一版。
  • 整批先檢查再裝:符號連結、檔名不是 kid、不是 Ed25519 公鑰(含誤放私鑰)任一筆不合格就整批拒絕、什麼都不裝。
  • 包內沒有 release-keys/ 不算錯。

出包守門:build_bundle.sh 要求 release-keys/PROD_KIDS 至少列一個 kid 且對應 .pub 存在,否則失敗;只有 --skip-prod-key-check(內部測試包,不可出貨)能略過(scripts/build/build_bundle.sh:174-191)。

撤銷的現況:主機端沒有「刪印樣」的機制。作廢一把章(換鑰完成、主力外洩)只影響之後簽出來的清單;已裝在客戶機上的舊印樣仍會被認。外洩情境要靠「不再用外洩的章簽新版」+「新版號遞增」讓舊章簽的東西沒有用武之地(見威脅對照)。

§5

guidant-release CLI

決策者電腦上的發布工具,Node 22 直接跑 .ts(cli/guidant-release.ts)。在 update-center repo 裡 npm ci 後 npm run cli -- <命令>,或 npm link 後打 guidant-release <命令>。

共用選項:

選項 說明
--site <網址> 更新站;也可設 GUIDANT_RELEASE_SITE。必須 https,只有 127.0.0.1/localhost 可 http(cli/site.ts:50-60)
--keys <資料夾> 印章資料夾,預設 ~/.guidant-release
--access-token-file <檔> 本機 wrangler dev 手測用的 Access 憑證檔(必須 600);不給就跑 cloudflared access token -app=<site> 取
--finish 只給 rotate-key 用

對更新站的身分:正式站送 cf-access-token(Access 驗過換成 Cf-Access-Jwt-Assertion 轉給 Worker);本機 127.0.0.1 直接送 Cf-Access-Jwt-Assertion。絕不送 Authorization(更新站會當 CI token、publish 一律 403)。redirect: "manual":Access 沒登入回 302 時當成「要先登入」處理。access token 與 passphrase 一樣不印、不寫檔。

子命令

命令 連更新站 要輸入 做什麼 來源
init-keys 否 主力章 passphrase ×2、備用章 passphrase ×2 資料夾已有 keys.json 就拒絕;產主力+備用、寫 keys.json、印封存說明 cli/commands.ts:83-126
publish <版號> 是 輸入版號確認、主力章 passphrase(換鑰中多問新章) 見下方流程 :165-213
withdraw <版號> 是 輸入版號確認 取詳情;已撤回就拒絕;POST …/withdraw :217-227
rotate-key 否 新章 passphrase ×2 已在換鑰中、或沒有主力章就拒絕;產 role: next 的新章,印接下來三步 :231-250
rotate-key --finish 否 — 新章沒有 endorsed_in(還沒隨任何一版發出)就拒絕;舊主力 → revoked、新章 → primary :252-267
promote-backup 否 備用章 passphrase、輸入舊主力 kid 確認 換鑰中不准;先解鎖備用章確認可用,再作廢主力、備用轉主力 :273-292
sync-keys 是 — 每把章 POST /admin/keys {kid, pubkey(base64 32B), status};next 登記成 backup;站上已作廢的作廢章視為已同步 :297-313
list-keys 否 — 列 kid、角色、建立/作廢時間、新章是否已隨版發出 :315-322

CI 呼叫的 register-draft 不在 CLI 裡,由 BE scripts/build/publish_draft.sh 打(見 更新站規格)。

publish 流程

%%{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 TD
  A["讀 keys.json:找主力章、有沒有換鑰中的新章"] --> B["GET /admin/releases/<v>"]
  B --> C{"欄位合理?<br/>版號一致、major=版號第一段、sha256 64 hex、<br/>size 正整數、min_base/min_agent 版號格式、has_security_fix 布林"}
  C -- 否 --> X["拒絕,什麼都不送"]
  C -- 是 --> D{status}
  D -- withdrawn --> X
  D -- draft --> E{"內部先裝:清單非空、<br/>每台 installed 且 result=succeeded"}
  E -- 否 --> X
  E -- 是 --> F
  D -- published --> F["同版重簽"]
  F --> G{"輸入版號確認"}
  G -- 不符 --> X
  G -- 符 --> H["passphrase 解鎖主力章(換鑰中再解新章)"]
  H --> I["buildPayload → canonical → 主簽+副簽"]
  I --> J{"用 .pub 驗剛蓋的章"}
  J -- 不過 --> X
  J -- 過 --> K["POST …/publish {signed_manifest, kid}"]
  K --> L{"回 status=published?"}
  L -- 否 --> X
  L -- 是 --> M["新章第一次隨版發出:記 endorsed_in"]
圖 2 — guidant-release publish:兩道關都在送出前、在決策者電腦上判
  • 第一道關「內部先裝」比更新站嚴:更新站允許空清單,CLI 空清單一律拒絕(沒人裝過不發,checkInternalInstalls,cli/commands.ts:133-143)。
  • 第二道關「自驗」:私鑰檔與 .pub 對不上(拿錯檔)在這裡擋,不是讓每個客戶都驗不過(verifyRelease,cli/envelope.ts:109-126)。自驗也核 payload_type、外層 kid 等於 payload kid、每個副簽章。
  • 更新站回的欄位不被信任:任何欄位不合常理就不簽(checkDetail,:145-156)。
  • 被拒時不會問 passphrase、什麼都不送。
  • 成功後 CLI 印「清單 90 天到期,到期前要重跑 publish 重新蓋章」(MANIFEST_VALID_DAYS = 90,cli/envelope.ts:21)。客戶端已不檢查清單有效期(見下節),這行提示與後台「快過期,請重簽」標仍按 90 天計算,但客戶不會因此拒收,重簽不是必要動作。

換鑰

換印章只能藏在一次對章通過的更新裡:客戶機用它現在認的舊印樣驗過一版,那一版的包裡帶著新印樣。

%%{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'}}}%%
stateDiagram-v2
  state "只有舊章(primary)" as S0
  state "換鑰中:舊章 primary+新章 next" as S1
  state "新章已隨版發出(endorsed_in 有值)" as S2
  state "新章 primary,舊章 revoked" as S3
  [*] --> S0
  S0 --> S1: rotate-key(新章 .pub 交給主產品進 release-keys/)
  S1 --> S2: publish 一版:舊章主簽、新章副簽、endorsed_keys 帶新印樣
  S2 --> S3: rotate-key --finish
  S3 --> [*]: sync-keys;舊章私鑰確認不需要後銷毀
圖 3 — 換鑰(舊章背書新章)
  • 換鑰中 publish:舊章主簽(signature)、新章副簽(cosignatures),payload 的 endorsed_keys 寫新印樣——被舊章簽進去=舊章背書新章。
  • 只認舊章的客戶機照常驗主簽章,不看 cosignatures(BE 與 shell 都只驗主簽章)。
  • 新章還沒隨任何一版發出就 --finish 會被拒:不然之後只用新章簽的清單,舊客戶機全部驗不過、再也升不上來。
  • endorsed_keys 目前沒有客戶端讀取;新印樣能被客戶機認,靠的是那一版包內 release-keys/ 經 release_keys_install 裝上去。

主力章出事

  1. 把封存的備用章私鑰放回印章資料夾(chmod 600)。
  2. guidant-release promote-backup:先用備用章 passphrase 解鎖確認可用,再輸入舊主力 kid 確認;舊主力作廢、備用轉主力。客戶機本來就同時認兩把印樣,立刻生效。
  3. sync-keys。之後沒有備用章:補一把新備用章(產章、隨包發出、封存)CLI 尚未支援。
§6

客戶端驗證

主系統(BE)

app/product_update/service/package_verifier.py。只信本機 GUIDANT_RELEASE_KEYS_DIR 的印樣;更新站、上傳者、包裡帶來的鑰一律不信。

順序 檢查 不過時 reason 來源
0 信封是字串、≤64KB;format_version = 2、alg = Ed25519;payload 解 base64、帶上限解壓;signature 嚴格 base64 bad_signature _verified_payload,:140-152
1 kid 查本機印樣:kid 必須 ^[0-9a-f]{16}$(kid 是路徑的一段,防 ../);<kid>.pub 不可是符號連結、必須是檔、≤4KB、必須是 Ed25519 公鑰 bad_signature(訊息:印章不在本機認得的印樣裡) _load_release_key,:112-123
2 簽章:Ed25519 驗 canonical;payload_type = release_manifest;payload kid = 外層 kid bad_signature :156-165
— 內容格式:必要欄位齊、issued_at 帶時區、version 與 major 一致、sha256 64 hex、size > 0、版號欄位格式;ops_* 格式不對當作沒有 bad_signature(內容格式不對) _manifest_from_payload,:176-200
3 版號遞增:現行版號可辨識,且清單版號(semver)>現行;不可降版或重裝 not_newer verify_manifest,:206-218
4 主版號相同:換主版號要重新授權 major_mismatch 同上
— 可直升:起點 ≥ 清單 min_base_version(更新站漏排必經版時在這裡擋) not_newer check_min_base,product_update_flow.py:467-471
5 下載完核包:檔案大小與 sha256 都要等於驗過章的清單;更新站或上傳者另外報的雜湊不採用 checksum verify_package_file,:234-241

清單有效期:沒有。檔頭明寫「清單不設有效期:客戶隔一年才升、離線檔放很久都要裝得起來;防重放舊版靠版號只能往上+主版號+本機印樣」(package_verifier.py:3);程式裡沒有 MANIFEST_VALID_DAYS,issued_at 只解析、不判。主機端 upgrade_order.sh:97 同樣註明「清單不設有效期(issued_at 不判)」。

呼叫點:

  • /v1/check 回來後逐段驗(_parse_available,product_update_flow.py:217-231):每段以前一段的版號為起點驗 3、4 與可直升,驗不過的那段整段不認、記原因。
  • 寫更新單前再驗一次(verify_ready_package,upgrade_order.py:146-162):清單版號必須等於包的版號,然後驗 1~5。
  • 讀「已經裝上的這一版」的清單(取 min_agent_version)用 read_signed_manifest:只驗章、不看版號,不可拿來決定收不收包(:221-223)。

主機(shell)

scripts/installer/lib/release_keys.sh。主系統容器無 docker socket,真正動手升級的是主機上的 guidantai;它在第 0 步(換成新版的自己)與預檢再驗一次,因為包與清單放在主系統可寫的 incoming/。

步驟 做法 來源
解信封 python3 優先,否則 perl 的 JSON::PP+MIME::Base64+Compress::Zlib;寫出 canonical 與 raw sig,印外層 kid、canonical 頂層 payload_type、頂層 kid。不用 grep(換鑰清單的 endorsed_keys 裡也有 "kid",grep 會取錯層) rk_envelope_parse,:44-65
驗章 openssl pkeyutl -verify -pubin -inkey /etc/guidant/release-keys/<kid>.pub -rawin -in canonical -sigfile sig;kid 必須 16 hex release_keys_verify_file,:25-31
型別與 kid payload_type == release_manifest 且 canonical kid == 外層 kid release_keys_verify_envelope,:72-86
讀欄位 一律從驗過的 canonical 讀(release_keys_canonical),不讀信封外層 :95-97
包雜湊 實際 sha256 = 包旁 .sha256 = 清單 sha256,三方相符;清單 version 格式合法 self_replace_verify,self_replace.sh
ops 小檔 雜湊比對清單裡的 ops_sha256;小檔旁的 .sha256 不採用;清單沒有 ops 欄位就退回從整包抽 sr_ops_verified,self_replace.sh
預檢 缺 python3 與 perl 模組時預檢直接失敗(否則每一包都會被當成簽章驗證失敗,症狀不指向這裡);可直升起始版也從驗過章的 canonical 讀 upgrade_preflight.sh pf_check_package、pf_read_min_base

cosignatures 不驗:舊客戶機只認主簽章,換鑰清單帶它與 endorsed_keys 照樣驗得過。

§7

測試章與正式章現況

項目 現況(2026-11-01 實查)
BE repo scripts/installer/release-keys/ 只有 b91819f86eb4b4f8.pub(測試章;用 openssl 從檔案算 kid 核對相符)
release-keys/PROD_KIDS 不存在 → 正式出包會被 build_bundle.sh 擋,測試期出包都帶 --skip-prod-key-check
測試章私鑰 首腦在決策者電腦用 openssl 產,放 ~/.guidant-release-test/(600、無 passphrase、不入任何 repo),只供內部測試;不是 guidant-release init-keys 產的,沒有備用章
正式章 尚未產生:要決策者親自跑 guidant-release init-keys,交付兩個 .pub 進 release-keys/ 並寫 PROD_KIDS
更新站已發布版 1.27.1~1.27.10 共 10 版,kid 全是 b91819f86eb4b4f8
更新站印樣表 signing_keys 0 列(未跑 sync-keys;不影響客戶驗章)
發布方式 測試期以測試章直接簽 188 產出的 release.json、直接寫 D1;未走 CLI publish 與內部先裝

換正式章時要注意:已裝測試期包的機器 /etc/guidant/release-keys/ 只有測試章。第一個正式章簽的版本要能被這些機器接受,必須先用測試章簽一版「包內帶正式章 .pub」的過渡版(等同一次換鑰),否則這些機器會以「印章不在本機認得的印樣裡」拒收;或由人工把正式章 .pub 放進這些機器。客戶出貨機一開始就裝正式章,沒有這個問題。

§8

威脅對照

情境 攻擊者能做什麼 不能做什麼(為什麼)
更新站被攻破(Worker 程式或 D1 被改) 對 check 回「沒有新版」(拖延更新);停發下載網址;把客戶回報資料讀走(只有授權編號、指紋、版號,無客戶名稱);把某版撤回 讓客戶裝上改過的包:清單簽章用本機印樣驗,改一個位元組就不過;改 sha256 欄位沒用,客戶只信驗過章的清單
同上 回一份舊的真簽清單(重放) 讓客戶降版或重裝:版號必須嚴格大於現行,且主版號相同
同上 把下載網址指向別的主機 主系統只接受 R2 帳號端點主機(DOWNLOAD_HOSTS);即使下載到別的東西也過不了 sha256
同上 在 signing_keys 登記假印樣 那張表客戶端不看
R2 被改(包或 .sha256 被換) 讓下載失敗 讓客戶裝上:包的 sha256 要等於驗過章的清單
CI token 外洩 登記新的草稿版號;覆蓋尚未發布的草稿 發布(publish 只收 Access 身分);覆蓋已發布或已撤回的版(409 not_draft);草稿只對內部先裝機器可見
Access 帳號被盜 撤回版本、改拒絕名單、指定內部先裝、送一份 publish 簽出客戶認得的清單(沒有私鑰);送去的偽造清單客戶驗不過
授權檔或身分票外洩 冒用那台機器的身分查版、拿下載網址 用別台機器指紋查(照綁指紋);拿到包也只是我們發布的原版
發布私鑰檔外洩(未含 passphrase) — 解開私鑰:PKCS8 以 passphrase 加密
私鑰+passphrase 都外洩 簽出客戶認得的清單 — 。處置:promote-backup 切到備用章,之後只用備用章簽、版號持續往上;主機端沒有「刪印樣」機制,被盜章簽的清單在客戶機上仍驗得過,只能靠版號遞增讓它簽的舊版號失去作用、並由拒絕名單/撤回限制更新站發放
客戶機上的人放入假印樣 讓該機接受自己簽的包 需要 root(/etc/guidant/release-keys/ root 755、檔 644);擁有 root 本來就能直接改系統
借簽:拿 LC 簽的授權檔或完整性 manifest 頂替發布清單 — payload_type 必須是 release_manifest(BE 與 shell 都擋);而且發布印樣與 LC 簽發鑰是不同的鑰
§9

相關文件