FR-167 · 技術規格 · 2026-11-01
發布清單的 v2 信封、canonical 與 Ed25519、kid、印章一正一備與換鑰、guidant-release CLI 全部子命令、主系統與主機的驗證順序、測試章與正式章現況、威脅對照
客戶主機要不要裝一個更新包,只看一件事:這份發布清單是不是用「客戶機上已經裝好的印樣」驗得過。私鑰只在決策者電腦,更新站與 CI 都沒有;客戶端不信任更新站、上傳者或包裡帶來的任何鑰。本頁以 update-center cli/ 與主系統 package_verifier.py、release_keys.sh 的程式碼為準。
| 角色 | 手上有什麼 | 能做什麼 | 不能做什麼 |
|---|---|---|---|
| 決策者電腦 | 發布印章私鑰(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 三方相符
格式與 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 帶上限,超過就丟(解壓炸彈)。
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| 項目 | 規格 | 來源 |
|---|---|---|
| 資料夾 | 預設 ~/.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)。產完印出封存說明:
.pub 留著)。.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 的規則:
release-keys/ 不算錯。出包守門:build_bundle.sh 要求 release-keys/PROD_KIDS 至少列一個 kid 且對應 .pub 存在,否則失敗;只有 --skip-prod-key-check(內部測試包,不可出貨)能略過(scripts/build/build_bundle.sh:174-191)。
撤銷的現況:主機端沒有「刪印樣」的機制。作廢一把章(換鑰完成、主力外洩)只影響之後簽出來的清單;已裝在客戶機上的舊印樣仍會被認。外洩情境要靠「不再用外洩的章簽新版」+「新版號遞增」讓舊章簽的東西沒有用武之地(見威脅對照)。
決策者電腦上的發布工具,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 打(見 更新站規格)。
%%{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"]
checkInternalInstalls,cli/commands.ts:133-143)。.pub 對不上(拿錯檔)在這裡擋,不是讓每個客戶都驗不過(verifyRelease,cli/envelope.ts:109-126)。自驗也核 payload_type、外層 kid 等於 payload kid、每個副簽章。checkDetail,:145-156)。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;舊章私鑰確認不需要後銷毀
signature)、新章副簽(cosignatures),payload 的 endorsed_keys 寫新印樣——被舊章簽進去=舊章背書新章。cosignatures(BE 與 shell 都只驗主簽章)。--finish 會被拒:不然之後只用新章簽的清單,舊客戶機全部驗不過、再也升不上來。endorsed_keys 目前沒有客戶端讀取;新印樣能被客戶機認,靠的是那一版包內 release-keys/ 經 release_keys_install 裝上去。chmod 600)。guidant-release promote-backup:先用備用章 passphrase 解鎖確認可用,再輸入舊主力 kid 確認;舊主力作廢、備用轉主力。客戶機本來就同時認兩把印樣,立刻生效。sync-keys。之後沒有備用章:補一把新備用章(產章、隨包發出、封存)CLI 尚未支援。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)。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 照樣驗得過。
| 項目 | 現況(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 放進這些機器。客戶出貨機一開始就裝正式章,沒有這個問題。
| 情境 | 攻擊者能做什麼 | 不能做什麼(為什麼) |
|---|---|---|
| 更新站被攻破(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 簽發鑰是不同的鑰 |
signed_manifest、signing_keys 欄位min_agent_version 的用法