Bonds.tw 有備而來 · Bond for the Future 2026-08-10

工程藍圖與進度

把白皮書第 5 章的「小密封罐」拆成可施工的模組:資料可驗、本人可驗、離線驗證、驗證者端、互通。 本頁追蹤每個模組的真實狀態——包含還沒開始的,以及還沒解決的。

草案 v1.6 · 2026-08-11 更新:實機端到端數字入帳(真卡出示 15 張 QR、撤銷檢查 865 ms);快照新鮮度上限;ZK 第七條 caveat;結果畫面重排
已完成
27 / 34 項工作
整體進度
79%
測試
1009 / 2 skipped(沒設 ZK_BENCH_DIR 時的實測 spike)
負責人
豆泥 / 全部里程碑

擋在最前面的那件事

上一版是 TW FidO SP 資格。測試憑證到位後,路障換人了——而且換成小很多的一個。

已解鎖:TW FidO 模擬測試憑證到位

取得了走行憑正式環境的模擬測試 SP 憑證,getSpTicket / getAthOrSignResult 現在可以呼叫,M2 到 M4 的開發不再需要等行政流程

但有個必須寫下來的但書:這組 sp_service_id 與規格書 v2.9 公開範例值 逐字元相同,是模擬測試系統的共用識別碼,不是 bonds-tw 專屬憑證。 開發測試完全夠用,但正式上線前仍須向 MOICA 申請專屬 SP。 白皮書 5.2 講的行政門檻沒有消失,只是不再擋住開工。

已解鎖:Xcode 27 可用(冷建置實測 30.83 秒)

原本 App Store 的下載是死的——這台跑 macOS 27 beta,而 App Store 供應正式版 Xcode。 改從 developer.apple.com 直接下載後解決。xcode-select 需要 sudo, 但用 DEVELOPER_DIR 環境變數指向 Xcode-beta 就能繞過。

已解決:實機安裝與硬體事實全部確認

實機安裝原本卡在 iOS 27 beta,根因是手機自己殘留的 provisioning profile—— 清 Mac 端與解除安裝都不會動到它們,要用 devicectl device profile list/remove 精確移除。 現在七項自我檢查在實機上全綠,包含模擬器答不了的兩項: 簽署金鑰確實在 Secure Enclave、憑證確實帶著 NSFileProtectionCompleteUnlessOpen

已解除:上游授權條款已補齊。這一欄先前寫著三個 ZK 元件的 repo 都沒有 LICENSE 檔,那句話從 2026-08-07 起就不再為真,而它在這頁上多留了三天—— openac-rsa-x509-swiftcce06e1a 補上 LICENSE-APACHE 與 LICENSE-MIT, go-zkid-verifier 同樣兩份,moica-revocation-smt 有 LICENSE。 依 PSE 書面回覆動工的那段期間結束了:現在第三方從上游原始碼就驗得到同一組條款。

真正該記在我們自己帳上的是:本專案的 Package.resolved 一直釘在 4a53fba——正好是補授權的前一個 commit——所以那三天是真的在對一份 沒有授權檔的快照建置。已 bump 到 cce06e1a;那兩個 commit 之間的差異 只有兩個 LICENSE 檔與 README 九行,沒有任何程式碼變動。

仍未解:組織 Apple Developer 帳號(要 D-U-N-S)與 bonds-tw 專屬 TW FidO SP,兩者都要走行政流程。目前用的 sp_service_id 與規格書 v2.9 的公開範例值逐字元相同,是模擬測試共用識別碼——開發夠用,正式上線不行。

已拍板的決定

先把幾個會反覆被問的問題定住,免得每次開工都重新吵一次。

D-01 主線是 bonds-tw/backupTW-iOS,不用個人 fork

mashbean/backupTW-iOS 與上游逐位元組相同(ahead 0 / behind 0), 沒有承載任何工作,只會多一層同步負擔。開發一律在組織 repo 開 branch 走 PR。 這是公共財專案,程式碼的所有權、授權條款、Apple 團隊、Pages 都該掛在組織底下,而不是個人帳號。

D-02 三個 iOS 專案收斂成一個,其餘轉為素材

backupTW-iOS 是唯一接上真實政府服務的(MyData → 加密 PDF → 身分欄位), 所以它是主幹。bond-tw-app(24 個 SwiftUI 畫面、設計系統)與 Bondbear-vanilla(完整的 ZK 驗證流程 UX、QR 掃描)都停在 2025 年 8 月、 且 ZK 部分是模擬的——把它們當 UX 藍本與畫面素材移植過來,不要三線並行維護。

D-03 ZK 只用 PSE 的成品,不自己造

privacy-ethereum 的那套(電路、Swift bindings、撤銷 SMT、Go 驗證器)每日都在更新, 且 openac-rsa-x509-swift 直接支援 iOS 16+ SPM 安裝。 我們的工作是「接上去」與「把線上流程改成離線」,不是重寫電路。

D-04 「本人可驗」與「資料可驗」是兩張憑證,不是一張

白皮書 5.2 明確要求把兩件事拆開。MyData 來的是欄位事實(無文件級簽章), TW FidO/MOICA 來的是持有證明與意思表示。 資料模型上做成兩個 credential、以同一把裝置金鑰綁定, 日後 MyData 補上文件級簽章時,只需替換前者的信任根,不必動整個皮夾。

里程碑

M0 到 M5 是有相依順序的:M2 卡住時,M0、M1、M3 的驗證器骨架仍可平行做。 每項工作的狀態就是現況,沒有預先灌水。

M0

工程基盤

豆泥 · 大致完成

讓這個 repo 具備承接後續五個里程碑的條件。小、雜、但每一項都會在後面擋路。

90%
  • 已完成 清空間並安裝 Xcode Xcode 27 已裝。xcode-select 需要 sudo,改用 DEVELOPER_DIR 環境變數指向 Xcode-beta 即可,本機建置實測約 2 分鐘。
  • 已完成 補上開源授權條款 Apache-2.0(相容 MIT 相依、對齊上游 mopro、含專利授權)。上游三個元件原本都沒有授權條款,2026-08-07 起已補齊(前兩者為 Apache/MIT 雙授權,後者為 LICENSE);本專案的 SPM pin 已一併 bump 到補授權後的 revision。
  • 半完成 Bundle ID 與簽章團隊轉組織 Bundle ID 已改為 tw.bonds.*。組織 Apple Developer 帳號仍未申請(要 D-U-N-S,與專屬 SP 一起送)。簽章團隊寫死在版控裡(project.pbxproj 八處 DEVELOPMENT_TEAM = 538MCM44UX,而 .gitignore 明確把 pbxproj 強制納入)——不是「留給各開發者設定」。
  • 已完成 部署目標拉到 iOS 16+ 原本依 target 混著 14.5 與 18.5,已統一為 16.0——OpenACSwift 的下限,同時保留最大裝置涵蓋率。
  • 已完成 修掉 PDF 解析的崩潰路徑 改為依標籤尋找欄位、容忍全形半形冒號、找不到退成 nil。實測舊版在空字串/單行/無地址區塊三種輸入都 SIGTRAP 閃退,新版全數通過。
  • 已完成 CI:build + test 本機沒有可用 Xcode,CI 是目前唯一能驗證「編不編得起來」的東西。已在 macOS runner 跑綠:BUILD SUCCEEDED、9 項測試通過。模擬器動態挑選,不寫死裝置名稱。
M1

資料可驗:MyData → 自發行 VC

豆泥 · 已完成

把「下載完就丟掉」的身分欄位,變成存得住、簽得了、拿得出來的可驗證憑證。

75%
  • 已完成 MyData 取件流程 WKWebView 導至 API.idPhotoRev、接手下載、解 zip、以身分證字號解密 PDF、抽出五個欄位。這是目前全專案唯一真正跑通的政府整合。
  • 已完成 欄位解析健壯化 已改為具名欄位比對+容錯,並補上 8 組測試(含多行地址、欄位順序調換、全形冒號、三種原本會閃退的短文件)。
  • 已完成 裝置金鑰與 Secure Enclave Secure Enclave P-256,模擬器退回 Keychain 且退回可被查詢而非靜默降級。附 IdentityReset——刪憑證不動金鑰,但「全部抹除」必須連金鑰一起刪,否則重新 onboarding 會拿到同一個 DID。
  • 已完成 W3C VC 2.0 資料模型與自我簽發 VC 2.0 + ES256 compact JWS。審查抓到一個會靜默失效的坑:只掛 credentials/v2 這個 context 時,JSON-LD expansion 會把統號、出生日期、戶籍地址全部丟掉而簽章照樣驗得過。已改用內嵌 context 並實測 expansion 產出全部 10 個 triple。
  • 已完成 加密持久化與一鍵刪除 檔案保護+排除 iCloud 備份+防路徑穿越。MyData 的明文中間產物原本留在 Documents 且被 Files App 公開,已移走並在解析後主動刪除。刪除控制項已接上設定頁。
M2

本人可驗:TW FidO 簽章與 ZK 證明

豆泥 · 已完成

接上 PSE 的 OpenAC,讓 App 能在本機產出「我持有有效自然人憑證」的零知識證明。

100%
  • 已完成 取得 TW FidO 測試憑證 走行憑正式環境的模擬測試 SP。但識別碼與規格書公開範例值相同,非專屬憑證——正式上線前仍須申請 bonds-tw 專屬 SP。
  • 已完成 以 node helper 驗證憑證可用 helper 已寫好並併入 scripts/twfido-sign.mjs,request 內容以本機 echo server 逐欄驗過。尚未實際完成一次簽章。坑:沿用的 Matters helper 寫死 op_mode=APP2APP,該模式不推播、只給深連結,從 terminal 跑 App 永遠不會有動靜;改用 SP-API-ATH-03 推播模式才會自己跳出。
  • 已完成 App-to-App 簽章流程 用戶端+回呼路由都完成。回呼被當成「叫醒」而非「結果」——任何 App 都能註冊同一個 scheme,所以簽章仍從伺服器取得並由 idp_checksum 認證,偽造的回呼最多讓我們多打一次請求。
  • 已完成 整合 openac-rsa-x509-swift SPM 相依已接上,九個 API 實測連結成功。二進位框架含模擬器切片,所以不受實機問題影響。授權條款原本缺席、依 PSE 書面回覆動工;2026-08-07 上游補上 Apache/MIT 雙授權,本專案的 pin 已 bump 到該 commit。
  • 已完成 選定 bonds-tw 專屬 APP_ID 55349ff540392a077ca3dcc9bbda4c3,由 bundle id 的 SHA-256 導出所以可複驗。
  • 已完成 電路與金鑰下載管理 可續傳、串流解壓、SHA-256 pin(已逐一比對 GitHub release 實際值)。撤銷清單刻意不 pin——它每天重新產生,錨點是鏈上 SMT root,而該檢查誠實標示為未實作。
  • 已完成 證明效能與記憶體實測 真實的行動自然人憑證簽章跑完整條路:prove 6.34+2.13 秒、verify 6.44+2.19 秒、linkVerify 7.06 秒且回傳 true。記憶體峰值約 1.8 GB、安裝後資產 1.96 GB。驗證比產生還慢——站在查驗現場等的是驗證方,這個數字直接影響 M3 的設計。
  • 已完成 從證明金鑰推導驗證金鑰 已在裝置上切檔並用真實證明驗收:推導出的位元組與下載檔 contentsEqual 相同,三項檢查對推導金鑰全數回傳 true,耗時 1.51 秒。更正上一版的數字:省下的是傳輸量 143.3 MB → 85.5 MB(57.8 MB),不是 968 MB;安裝後仍是 1.96 GB,因為檔案照樣要在磁碟上。真正的重點也不是流量——是驗證用的信任材料不再取自會變動的 -latest tag。
  • 已完成 修掉「約束未滿足卻回報成功」 上游 provetbs 與簽章不符時仍回傳正常大小的 ProofResult(電路 assert 只印在 stderr),這是我們改不動的上游行為。改掉的是我們怎麼講它:驗證階段的 Rust panic 現在被辨識為「證明被拒絕」而非 .unknown,且 ZKProver 根本不會把被拒絕的證明交出來。畫面不再說「這支手機無法完成驗證」——第 3 階段照實顯示成功(兩組電路都跑完了),第 4 階段顯示「這支手機檢查了這份證明,並判定不通過」。
  • 已完成 四個階段的措辭稽核 順手修掉兩個同類問題:每次成功執行其實驗證了兩次(報告拿到的是「熱」的第二次數字),以及推導檔案傳輸成本為零卻沿用下載措辭、對使用者顯示「下載 0 KB」。三項修正都以突變反向確認過會讓測試轉紅。
M3

離線驗證:把 No Phone Home 做成真的

豆泥 · 進行中

範圍已收斂:只做「驗證離線」,不做「發證離線」。 發證階段需要 MyData 與 TW FidO,本來就得連網;要離線的是查驗現場那一端。 這是整份藍圖最需要自己動手的一段——PSE 的流程是線上的:跟伺服器要 challenge、產證明、 POST 回去驗證並在伺服器端做 nullifier 去重。白皮書 5.2 要的卻是離線點對點、不回傳、不留軌跡。 順序上排在 M2 跑通之後。

75%
  • 已完成 challenge 改由驗證者產生 驗證者當場產生 challenge,持證者用裝置金鑰簽的 VP 涵蓋它。成功與失敗都消費掉該 challenge——失敗仍保留等於給攻擊者對活體 challenge 的無限次嘗試。
  • 已完成 本機驗證路徑 整條查驗路徑零網路呼叫。補上了 did:key 反向解碼——先前驗證者拿到 issuer DID 卻還原不出公鑰,等於什麼都驗不了。雙層驗簽(VP + 內層 VC)+ challenge 比對 + 時效。
  • 已完成 ZK 證明的出示與查驗 在這之前 M2 與 M3 是兩條互不相通的路:ZKProofBundle 全 app 只有最小揭露畫面引用,整個出示層不知道它存在——能產證明但出示不了,能離線出示但(自發行 VC 帶固定 did:key)可被連結。現在有了封裝格式(四個檔案+caveat+持證方自檢)、查驗方 ZKPackageVerifier、以及首頁的「查驗零知識證明」入口。見證檔案永遠不會被打包(有測試釘住),caveat 一律攤開顯示不收在展開箭頭後面。
  • 半完成 點對點傳輸層 QR 與藍牙都已完成,兩個方向都通過實機驗證(2026-08-13,iPhone 14 ↔ Mac,對齊 ISO 18013-5 的 mdoc peripheral server mode:持有人廣播、查驗方連線)。Wi-Fi Aware/NFC 仍未做。先前這一格的三個數字都是錯的,一併更正:ZK 證明交到查驗方手上的總量,用真卡量是 398,181 bytes(不是 293,916——那是拿上游範例輸入量的);而決定張數的不是 QR v40 的 2,953 bytes,是本專案為了隔著螢幕掃得到、把符號釘在 89 modules + ECC Q 之後的 364 bytes。所以是 824 張、一輪約 453 秒,不是約 100 張——而且根本走不到那一步,QRTransport 對超過 64 KB 的 payload 直接拒絕、連分片都不分。QR 那條路不是慢,是不存在。同一份走藍牙是 597 frames、21.72 秒(實測 13.8 kB/s 穩態;先前推估的 8.6 秒是拿 12 frames 的暖機速率外推,碰不到 back-pressure)。
  • 技術驗證 Spike:離線的唯一性怎麼辦 仍未決。這一輪做的是 VC 出示,不是 ZK 的 nullifier,所以還沒碰到。但已經確認一個更根本的問題:自發行 VC 帶固定 did:key,每次出示都可被連結——與白皮書的不可連結性衝突,解法是 ZK 選擇性揭露。
  • 半完成 撤銷清單離線快照 卡片簽章路徑已完成。這一欄先前整個被記成擋在上游,而那只對 ZK 路徑成立——查驗方直接驗卡片簽章時,手上就有持卡人的憑證,而 MOICA 的撤銷樹就是用憑證序號當 key。createSmtProofFromGz 早就在 API 裡,快照也早就在下載,中間那段程式碼是唯一缺的東西。
    誠實的界線寫進了程式碼與畫面:裝置能確立的是「這個序號不在這支手機持有的這份快照裡」,而快照自述了製作時間;不能確立那份快照是真正的當前版本(那要讀鏈上 root,是網路呼叫)。所以畫面永遠不說沒有限定語的「未被撤銷」,並多印一行名單製作日期。查到序號在名單裡則整份拒絕——過期名單會漏報,不會誤報。
    ZK 路徑仍擋在上游:要把證明綁上快照需要 smtRoot 這個公開輸入,而那些訊號過不了 iOS FFI(zkID#105)。實驗確認過 smtRoot 確實是公開輸入(只改它重產證明,instance 有 274 個穩定位置跟著變),但知道「哪些位元組會動」不等於知道怎麼解出 root。需求仍是一句話:請在 iOS 靜態庫匯出 signals accessor(Rust shim 早就有 zk_last_signals(),是 mopro.swift 的公開介面只從 linkVerifyverifyCertChainRs4096verifyUserSigRs2048 回 Bool)。先前這裡寫的是「請上游公佈 instance 的序列化格式」——那是在要一個早就給了的東西go-zkid-verifier/verifier/public_inputs.go 逐行寫出三種佈局(RS2048 19 個訊號、RS4096 36 個、user_sig 4 個),本專案的 ZKPublicInputs.swift 也已經照著重寫並用 Go 的向量對過。要錯東西的請求,會一直掛著沒人回。
M4

驗證者端與可示範場景

豆泥 · 已完成

沒有驗證者,皮夾只是個人收藏。5.2 承諾要釋出一個「不連雲端、不記名、不留軌跡」的輕量驗證介面。

100%
  • 已完成 驗證者 App(或同 App 的驗證模式) 走「同 App 的驗證模式」:憑證查驗(掃 QR)與 ZK 證明查驗(讀檔案)都在首頁「離線出示與查驗」底下。三種結果嚴格分開——「這支手機還沒下載查驗檔案」是查驗方自己的設定問題、「跑了但無法判定」什麼都沒說、只有三個布林全 true 才是通過,而且通過也一律把 caveat 攤開顯示。
  • 已完成 官網上的 ZKP 驗證展示頁 已上線:mashbean.net/zkp-verify/(先掛在部落格測試,之後再整合進 bond-website)。刻意不做完整密碼學驗證——那需要解壓後約 968 MB 的驗證金鑰,瀏覽器分頁裝不下;而且實測是計算受限而非 I/O 受限(968 MB 冷讀 0.59 秒、三項檢查約 14 秒),所以「先下載金鑰再驗」也解決不了。做一個假裝能驗的頁面,正好違反這個專案一直在守的線。
    它做真的做得到的事:解開 App 匯出的 .zkproof,列出四個檔案與大小、換算 QR frame 數、顯示持證方自述(並標明那是被查驗一方的說法、可能是假的),以及逐條列出證明自帶的但書。檔案不上傳。但書文字與 App 的 ProofCaveat.localizedDescription 逐字對齊。
  • 已完成 三個示範場景 能力宣告寫進程式碼而不是投影片——demo 是最不歡迎但書、綠勾最有說服力的場合。結果三個裡只有一個完全成立
    滿 18 歲:一半,而且先前那句「做不到」是錯的。電路那半仍然成立(兩個電路吃的是憑證鏈與 RSA 簽章,輸入裡沒有任何日期欄位),但把它寫成「這件事做不到」是把「目前這條路做不到」講成了別的意思。現在的做法是在發證當下由行憑一起簽一個述詞欄位 over18AtIssuance——名字指的是「那一刻為真」,因為卡片簽的是當下的事實——出示時可以只揭露那一項而不揭露出生日期。仍然是一半:憑證帶著持卡人姓名(見下方風險欄),所以這不是不可連結的年齡證明。
    真人且唯一:一半。「真人」成立;「且唯一」是 noGlobalUniqueness,離線沒有共享的已用 nullifier 紀錄。
    曾是台灣人:成立。這正是目前證明說的話,也是緊急期場景。
  • 待辦 信任清單治理 格式與正規形式已寫完、測試釘住,但在 app target 裡零呼叫TrustList 唯一的非測試引用就是它自己的定義,沒有內建承諾值、沒有隨 bundle 出貨的清單檔、expectedCommitment 是一個沒有人傳值的參數。同一個 codebase 的 OfflineVerifier 原話是「這台裝置上沒有信任清單可以評估第三方簽發者」,並對任何第三方簽發者一律拒絕——所以白皮書 5.2 的緊急換發證者,出貨的 App 做不到。「屆時什麼都不用改」也不成立:沒有 supersedes、沒有指名的下一個簽章者、沒有回滾規則、沒有單調性。2026-08-13 還在這個格式裡找到並修掉一個可複現的承諾值碰撞(一則 note 塞換行就能偽造整行),並定下一條紅線:在接續機制做完之前,任何承諾值都不可以公開。
M5

第三方皮夾:把台灣數位憑證皮夾(TWDIW)整合進來

豆泥 · 相依 M1 · 2026-08-16 重新定義範圍

原本只寫「資料模型留好接口」。範圍已擴大成真的做一個第三方皮夾:官方認證的發行者與驗證者都能跑完整流程,皮夾裡除了我們自己的身分證,還有駕照電子卡等官方憑證。目的不是相容性打勾,是證明最理想的皮夾長什麼樣子——把三種信任模型並排,讓它們的差異第一次變得可見。完整規劃在 docs/twdiw-integration-plan.md

34%
  • 已完成 規劃與事實查證(2026-08-16) 八項事實全部自己量過、可複現。三個決定性的:
    ①「第三方皮夾」在 TWDIW 裡不存在。 發行端與驗證端都要申請沙盒帳號、自架、做 UAT;皮夾沒有申請表、沒有註冊、沒有 attestation。denkeni 手冊裡標題叫「If You're Building a Third-Party Issuer or Verifier Software」的那一節是空的,而第三方皮夾連標題都沒有。
    client_id 不是名單,是回音。 證據鏈全在 MIT 授權的官方原始碼裡:發行端自己簽的 ID Token 把 aud 寫死成 "moda_dw"CredentialService.java:1715)→ OID4VCI handler 從那個 aud 讀出 client_idCredentialIssuer.java:225)→ /token 拿它比對(:1196)→ proof 的 iss 只跟它比(:1741)。整條鏈驗的是「你送的字串等於你送的字串」。 對我們是入場沒有技術門檻;對 TWDIW 是一個要回報的缺口——憑證與「哪個皮夾拿走它」之間沒有任何綁定。真正被驗的是金鑰:proof 簽章會用 DID 取出的公鑰驗過,那把公鑰寫進 cnf.jwk裝置綁定是真的,皮夾身分是假的。
    ③ 我們現在的 DID 會被官方拒絕。 TWDIW 用 multicodec 0xEB51jwk_jcs-pub,varint D1 D6 03),payload 是整份 JWK 的 JSON;我逐位元組解過兩個真實 DID(129 bytes,前 3 是 D1D603,其餘 126 是 JWK 文字)。官方後端的解法是 did.substring(1) → Base58 → substring(6)「去掉prefix D1D603」——寫死三個位元組、不檢查 multibase、不檢查 multicodec。我們的 DIDKeyp256-pub(0x1200)產 did:key:zDn…,送過去必然解析失敗。
  • 已完成 M5.1 did:key 第二種拼法+解析寬容規則 新增 jwk_jcs-pub 編解碼,與現有 p256-pub 並存(我們自己那條離線路徑的 DID 不改)。
    而且官方皮夾自己的 DID 不符合它宣稱的編碼jwk_jcs-pub 的定義就是 JWK 經過 JCS(RFC 8785),鍵序必須 crv < kty < x < y。機關 DID 是 crv,kty,x,y ✅;官方皮夾的 holder DID 是 crv,x,y,kty。我們的 DIDKey 有一個 nonCanonicalDID 錯誤案例,存在的理由正是拒絕這種東西——所以嚴格照規範實作的解析器,會拒絕真實世界唯一存在的那個台灣皮夾。
    由此定一條不對稱規則:我們自己產生的一律 JCS 正規;解析別人的一律不要求正規。 正規化是我們對自己的義務,拿它去否決別人只會讓我們無法互通。
    閘門:用官方那個非正規 holder DID 當測試向量,必須解得開。不碰網路,可離線做完。
  • 卡住 M5.2 OID4VCI 領卡(沙盒)——卡在兩個必須先決定的事 五步實測順序:credential-offer-object.well-known/openid-credential-issuer/token(pre-authorized_code + tx_code)→ /credential(proof JWT,typ: openid4vci-proof+jwtaud 結尾有斜線)→ 抓卡面圖。CredentialStore 本來就是一檔一憑證、有 allIDs()結構上已經是多卡的,不必改。
    ⚠️ client_id 要送什麼是「先量再決定」,不是先決定。moda_dw 一定會通,但不該長期冒用官方 App 的識別字串。做法是先送 tw.bonds.backupTW 量它會不會被拒:通過就用我們自己的;被拒的話,那件事本身就是要回報的發現——TWDIW 沒有辦法讓第三方皮夾正當地表明身分,於是唯一能跑的方式是冒充。
    前置:沙盒帳號申請的對象是發行/驗證端,但 demo 站的「訪客領卡」不需要帳號,可以先不申請就跑通。要不要申請、要不要按同意服務條款,由使用者決定。

    ⛔ 2026-08-16 對抗式查證擋下兩件事。② 已完成,① 卡在一個必須先做的量測:
    (進行中:卡片層已完成——StoredCardSource,順手抓到「出示會靜默改指第一張官方卡」這個活著的缺陷。發行者授權閘門已完成——兩道閘門、主機當主機比不用前綴、無法正規化的一律拒絕、比中之後改用清單自己的字串。① 一卡一金鑰卡在紅線:整套 Keychain 列舉壓在一條從來沒量過的假設上(SecItemCopyMatching + kSecMatchLimitAll 看不看得到 Secure Enclave 金鑰、tag 與建立時間回不回得來),而漏看一把的後果是靜默的——掃描刪 0 把、驗證通過、使用者被告知已清除。本專案的規矩是先量再決定,所以停在真機量測前面。)
    ①「這個發行者是誰」沒有人檢查。 領卡流程的 URL 全部來自一張 QR,而其中一步會用持有人金鑰簽一個 proof JWT、aud 直接填對方給的值。任何人印一張 QR,就能讓皮夾對他指定的 URL 產生一個由裝置長期金鑰簽出來、內容由他選的 JWT。 本專案對這類輸入已經有立場(MOICACallbackRouter 檔頭:inbound URL 是 untrusted input,只能當「再去查一次」的訊號)。而那 43 筆信任清單正好可以拿來比對 issuer DID。
    ② 要用哪一把持有人金鑰。 若沿用現有那把唯一的 DeviceKey,TWDIW 憑證的 cnf.jwk 會被寫進政府發行端的資料庫,而那串位元組正是任何一個離線查驗方看到的識別碼。我們刻意讓自然人憑證那條路拿不到統一編號,結果會從另一扇門交出一個更好用的索引——而且這條汙染不需要任何人上網才生效,離線出示的那張卡上就帶著這個公鑰。→ 至少一卡一金鑰;而一旦有第二把,IdentityReset 只吃單一 keyTag 就不夠用了,「重設身分」會漏掉 TWDIW 那幾把。

    ✅ 2026-08-16 實機量測:① 的紅線清掉了。 iPhone 14 實機,五題全部是好消息——列舉看得到 Secure Enclave 金鑰(tokenID=com.apple.setoken)、kSecAttrApplicationTag 帶得回、kSecAttrCreationDate 帶得回、用 kSecValueRef 刪得掉、掃完剩 0 把。所以「列舉當真相來源不必維護清單」「用建立時間當護欄避免刪到領卡中途那把」都成立。
    ⚠️ 模擬器不能拿來回答這一題:同一支測試在模擬器上五題也全部「通過」,因為它會回報 com.apple.setoken 而那裡根本沒有 SEP。測試裡因此多一支專門在模擬器上印警告的,免得一次綠燈被當成答案。
    ② 已完成:兩道閘門、主機當主機比不用前綴、無法正規化的一律拒絕、比中之後改用清單自己的字串。
  • 已完成 M5.3 SD-JWT VC 讀取與選擇性揭露 TWDIW 把 _sd 放在 vc.credentialSubject 底下,不是 SD-JWT VC 標準的頂層 claims(沒有 vct,用 vc.type)——是 VCDM 1.1 與 SD-JWT 的混血。我們的 SelectiveDisclosure 已經是同一套加鹽承諾機制,接得上,但要另寫 adapter,不要把我們的格式改成它的
    做完了,而且比預期簡單——因為密碼學早就是共用的。 SD-JWT 的摘要是對「揭露的 base64url 字串本身」做 SHA-256,不是對解碼後的 JSON,而本專案的 Disclosure.digest 早就是這個算法。而且不是照規範推的,是拿正式資料驗的:用 2025-10-07 一張真實駕照電子卡的六段揭露,重算出它 _sd 公開的那兩個值(id_numberroc_birthday),兩個都對得上;對解碼後的位元組做雜湊則一個都對不上。
    順帶抓到自己抄錯的三個值:姓名是陳筱玲不是陳籍玲、身分證是 A234567890 十碼不是十一碼、管轄編號中間沒有空格。三個都是因為從渲染後的頁面上讀而不是把 base64 解開。渲染過的內容不可以當資料來源——一個十一碼的「身分證統一編號」留在文件裡,後面每個讀它的人都會被誤導。
    釘住的:拒收未被承諾的揭露(紅線)、少給揭露是正常的、alg 不是 ES256 一律拒、_sd_alg 不認識就拒不要假設、statusListIndex 在正式環境是字串(只當數字讀會靜默丟掉整個 status 條目,而沒有 status 的憑證就是永遠不會被查撤銷的憑證)。1,060 個測試通過。
  • 待辦 M5.4 OID4VP 出示 POST /api/oidvp/authorization-response,帶 statevp_tokenpresentation_submission。vp_token 的 aud 形狀是 redirect_uri:https://…(前面真的有那個字首)。
    ⚠️ 官方的 vp_token 裡是 "vp":{"context":[…]}——沒有 @(我從真實 vp_token 的 base64 解出來確認過)。JSON-LD 處理會把整個 VP 的主張丟掉而簽章照樣驗得過,正是 M1 咬過我們那個 bug 的同一個家族。要不要複製這個錯字?——出示要被官方驗證端收下就得照它實際吃的格式;但必須在程式碼註明哪一天、為什麼、上游修好後拿掉。一個沒有註解的相容性 hack,三個月後會被當成我們自己的 bug 修掉。
  • 半完成 M5.5 三種卡並排與能力對照 這一項才是整個 M5 的論點。 「最理想的皮夾」的區別不是裝得下很多張卡——官方 App 也裝得下——而是它把每一張卡證明得了什麼、證明不了什麼,寫在卡的旁邊
    我們有硬證據說明為什麼非做不可:官方那一套分不出 IAL。 denkeni 自己問「是不是用 kid: key-1 標示 IAL?」自己回答「nope」;實測 IAL-1(自己在網頁填的 email)與 IAL-3 的 VC 結構上一模一樣——同一個 jku、同一個 kid、同一個發行者 DID。皮夾看不出「櫃檯驗過證件」與「使用者打字」的差別。 加上撤銷是每天過期的線上清單(實測 nbfexp 正好 86,400 秒)——離線超過 24 小時,TWDIW 憑證的撤銷狀態不可知
    要新增兩條既有 caveat 型別沒涵蓋的:「這張卡看不出身分確信度」與「離線超過 24 小時撤銷狀態不可知」。⚠️ LocalizationCoverageTests 目前不涵蓋 PresentationScenario(UX 盤點 b-1 的同一個洞),新增之前要先補覆蓋,否則新的英文字串會照樣全綠。

    ✅ 2026-08-16:論點做出來了,畫面也畫上了。 CardCapability 三種卡並排,接在能力頁的場景之後——上面問的是「這個 App 證明得了 X 嗎」,下面問的是「我的每一張卡值多少」,同一個問題的兩端。
    一條紅線用測試釘住:任何一張卡都不准說「與你其他的卡不可連結」。一卡一金鑰切斷的只有卡與卡之間;TWDIW 的 cnf.jwk 由發行端固定、每個查驗方永遠看到同一把,statusListIndex 逐張唯一又在揭露信封之外,而在這些之前——駕照上的姓名早就把兩張卡接起來了。測試同時檢查英文鍵與出貨的中文,因為一句只在翻譯後才出現的承諾仍然是承諾。
    順序也是斷言:我們的排第一。把政府那張放前面,底下的限制會讀成在抱怨別人。
    b-1 的教訓有照做:翻譯與覆蓋測試跟字串同一批進來。畫面 render 成 PNG 逐頁看過(3,655pt 高、五頁),三張卡的「可以倚賴的」與「建立不了的」是同樣的視覺重量——那正是論點。


    ✅ 同日補上首頁的卡片列。 在此之前首頁只有一列,寫死自發行的那一張——TWDIW 卡收下來、存進去,然後在首頁上不存在。這跟「MyData 收完首頁沒反應」是同一類缺陷,後果也一樣:一個沒有任何證據顯示卡到手的人,會再去領一次,而政府核發的卡可能意味著再跑一趟櫃檯。
    規則寫在 CardInventory(純函式、可測),不寫在畫面裡,因為其中三條錯起來在截圖上完全看不出來:①讀不出來的卡照樣有一列(誘人的寫法是 compactMap——清單很乾淨,而卡憑空消失);②順序是我們定的,不是 store 的目錄順序(那種順序「穩定到看起來像刻意的、又不穩定到會在無關的寫入之後重排」,是最糟的組合);③欄位值一律不上首頁,卡的種類才上——一個測試拿 TWDIW 官方範例人物的姓名、身分證字號、生日去比對整列文字。
    做的過程撞到兩個真的會出事的東西:(a) Item 的識別是「標題+副標」,而兩張同月核發的同型政府卡兩者都一樣——NSDiffableDataSourceSnapshot 遇到重複識別是直接終止 App,不是畫錯。Item 因此多了 identifier(b) StoredCredentialViewController 綁死在單一識別碼上,所以「點政府卡就 push 詳細頁」會在你點的那一列底下顯示另一張卡的內容。現在點下去直說這個版本列得出、打不開,並給出它唯一真的答得了的問題(這種卡證明得了什麼)。沒有畫面的功能寧可直說,不要假裝。
    沒有 exp 的卡也另外處理:reader 會代入 .distantFuture,直接印出來會變成「有效到 4001 年」——把發行者從來沒做過的宣稱變成螢幕上的一個日期,正是這個 App 反對的那件事。
  • 研究 兩張卡的事實剛好相反,而這是產品的核心對比 官方駕照電子卡實測帶 id_number(可選擇性揭露)。對照我們自己量過的:自然人憑證的 Subject DN 裡沒有統一編號(只有 C/CN/serialNumber 16 位數字)。所以我們那條路扣不住姓名,也拿不到號碼;TWDIW 兩個都有。
    皮夾裡會同時放著兩種相反的卡:我們的離線可驗但無法不可連結,官方的欄位齊全但要信任發行者且離線一天後撤銷不可知並排本身就是論證,統一格式會殺掉論證——所以明確不做「把自發行憑證改成 TWDIW 格式」。
    資料風險隨之而來:把官方駕照放進來,等於把一個我們自己刻意拿不到的識別碼帶進裝置。LocalDataEraser 必須一體涵蓋,出示預設全關對 TWDIW 卡一樣成立,而且卡片說明要直說「這張卡裡有你的身分證字號」。
  • 研究 政策風險:現在不需要許可,不表示以後不會 TWDIW 目前沒有辦法分辨皮夾,但歐盟 ARF 走的是 wallet attestation。若 TWDIW 補上,第三方皮夾會在一夜之間從「不需要許可」變成「需要許可」。所以排程上要早做,而且「我們用了什麼身分字串」要集中在一處,方便將來換。
    順帶:這件整合會產出四份可以回報上游的發現——client_id 不具鑑別力、官方皮夾的 did:key 不符 JCS、issuer metadata 的 format 與實際回傳不符(宣稱 jwt_vc_json、實際是 vc+sd-jwt~ 揭露串,照 format 解析的客戶端永遠看不到揭露)、VP 用 context 而非 @context。後兩者都屬於「簽章驗得過、語意卻是錯的」,最難被自己的測試抓到
  • 半完成 VP 呈現與 Schema 標準化 選擇性揭露的底層機制完成(SD-JWT 式加鹽承諾)。這曾是「滿 18 歲」真正的擋路點——不是電路,是資料模型:當時出示會交出整個 credentialSubject,等於為了證明年齡而連身分證字號與戶籍地址一起給出去。那一點已經解掉了。
    三個安全性質都以突變反向確認過會轉紅:每條揭露各加 128 bits 鹽(否則 nationality 十來種、birthdate 約四萬種可能值都能枚舉還原)、承諾摘要排序(否則位置洩漏哪個摘要對應哪個欄位)、拒收未被承諾的揭露(否則持證方能宣稱任何事)。已接進出示流程與 UI:出示前每個欄位一個開關、預設全部關閉,按鈕寫「出示 N 個欄位」,查驗方會看到「另外保留了 N 個欄位」。credentialSubject 現在只剩 id,其餘全進 _sd——留在明文裡的欄位就是持有人永遠無法扣住的欄位。
    但有一個欄位扣不住,而畫面一度承諾扣得住:姓名寫在替憑證簽章的 X.509 憑證的 Subject CN 裡,而查驗方需要那張憑證才能驗簽。X.509 沒有選擇性揭露,所以這是結構性的。該開關已經拿掉,改成一行「一定會被看到」的說明。還沒對齊 OID4VP 的線上格式。
  • 已完成 TW-DIW 相容性評估 2026-08-16 做完,結論被上面的 M5.1–M5.5 取代。原本寫的是「若官方走 SD-JWT,需同步建立去連結化路徑,否則授權後的資料包會變成新的蜜罐」——官方確實走 SD-JWT,而那個顧慮的形狀跟預期不同:蜜罐不在「授權後的資料包」,在憑證本身(駕照電子卡直接帶 id_number),而且它是選擇性揭露的,所以出示時扣得住、儲存時扣不住。去連結化路徑對這一點沒有幫助,能幫上忙的是儲存與抹除的一體涵蓋,以及卡片上直說裡面有什麼。
    明確不做的四件事也在這次定案:不做第三方發行端/驗證端(要自架、要 UAT,我們的價值不在那裡);不把自發行憑證改成 TWDIW 格式(會把離線保證稀釋成它的等級,並殺掉並排的論點);不把 TWDIW 信任清單當成我們的 TrustList(它是 HTTPS API 加 Arbitrum 錨點 0x84172caf…,跟我們的離線承諾模型是兩件事);不做 ZK 與 TWDIW 的接合(會產生一個看起來像零知識、實際上要信任發行者的東西,比不做更糟)。

介面與可讀性

2026-08-13 的全面盤點。五個視角平行跑(櫃檯那二十秒/無障礙/第一次打開/失敗與拒絕/中文文案),原始 42 條,合併後 40 條、9 組同根因。排序判準刻意不是 S/M/L,是:(a) 會不會讓人在櫃檯前做錯判定 → (b) 會不會讓但書讀不到 → (c) 會不會讓真的需要它的人用不下去 → (d) 其他。完整清單在 docs/ux-audit-2026-08-13.md

✅ 2026-08-16:(a)「會讓櫃檯前的人做錯判定」與 (b)「會讓但書讀不到」兩band 全部關閉,共 11 條。 它們散在四個檔案,卻是同一件事的切面:畫面說的比系統擔保的多,或者把該重的講輕了。最能說明這一點的是 a-5——over18AtIssuance這個 App 自己簽發並簽章的欄位,卻因為沒被 ClaimLabel 認得而穿上那套專門標記「陌生人取的欄位名」的引號樣式。代價不是難看:那套樣式的價值全在它稀少,里長看到 App 對自家欄位也這樣穿,下次真正可疑的欄位出現時就不會有反應了——用自家欄位去花掉保護自家的訊號。另一組(b-2+b-5)則是「字都在、位置不對」:判定與六條但書被一個已經作廢的配對 QR 推到摺線以下(AX5 實測下方 322pt),六條還串在同一個 363 字的 UILabel 裡——VoiceOver 一滑跳過全部,也無法重聽任何一條。這個套件裡沒有任何斷言抓得到這一類缺陷,所以留了一支預設停用的 render scratch。

✅ 2026-08-16 續:(c) band 全數關閉(21 條),(d) 的組 H 兩條也做完。 整個盤點 40 條,剩 6 條——全是術語收斂(同一個欄位兩張表、同一個角色六種叫法)。後面幾批裡最值得記的三件: isIdleTimerDisabled 全 repo 零筆,而輪播與 21.7 秒的藍牙傳送都是「按下最後一次之後完全不碰螢幕」,低電量模式強制 Auto-Lock 30 秒不可更改——新的 ScreenWakeLock計數的,因為那個布林是 process 全域的,「appear 設 true、disappear 設 false」在兩個畫面重疊那一刻就錯了。 藍牙重組失敗時 collector 手上是一組完整的 frame,重試的每一張都被判 duplicate 丟掉——傳輸永遠不可能再成功,而畫面停在「接收中… 100%」,因為 collector 真的就是這麼認為的 第一次使用那條路畫出五個身分欄位、緊接著把人送去行憑 App(正是 iOS 拍 app switcher 快照的時刻),而它不在隱私遮罩清單上;旁邊那個畫面的註解寫著「已由 PrivacyShield 處理」——一句關於它自己、而且是假的敘述。另外,FailureFaceRenderScratch 裡有一條測試把 c-1 的沉默寫成期望值,一個被記錄下來的 bug 就是這樣變成一個被保護起來的 bug,它今天真的擋了一次。

✅ 盤點結案:原始 42 條、合併後 40 條,全部處理完畢(1,139 測試綠)。 最後一批是術語收斂,其中 d-3 值得單獨記:持有人勾的開關寫「身分證字號」,里長螢幕上出現「統一編號」——同一個已簽章的位元組,兩個名字,而這整條流程存在的理由就是兩個人在看同一件事。留哪一個名字不是隨便選的:查驗方的工作是拿螢幕跟一張實體國民身分證字對字比對,所以要用卡片上印的那個詞(統一編號、出生年月日)——這條規則在兩處選擇了違反多數,而那正是重點:要求是對得上卡片,不是對得上程式碼裡碰巧比較常出現的寫法。

最要緊的三條是同一個故事,2026-08-16 一起修完:a-2 讓誠實的人被拒絕,b-4 讓緩解的句子幾乎看不見,a-3 讓其中一種情況連那句都沒有。

  • 已修 a-2 從掃描器返回,會指控誠實的人在重播 viewWillAppear 無條件換掉挑戰,而 viewWillDisappear 刻意不取消(註解自己寫了理由:推出掃描器不該取消那個掃描器正要收答案的挑戰)。所以畫面離開時保住挑戰、回來時把它換掉。按返回、相機權限被拒、任何一趟來回,對方那份誠實的出示就變成 challengeMismatch——而那個失敗在畫面上寫的是「可能是先前出示的複本」。把該通過的判成不通過,而且承擔形式是一句指控。 兩個視角獨立撞到同一個地方,是全部 40 條裡證據最強的一條。
    修法:兩半的判斷抽成 VerifierScreenLifecycle,比照持證方那邊的 PresentationScreenLifecycle。單看任何一半都合理,bug 在它們的分歧裡,所以測試有一條專門釘住「departure 保住的東西,arrival 不可以丟掉」。單一使用規則沒變——真的離開仍然取消。
  • 已修 b-4 唯一保護誠實使用者的那句話,是全畫面最淡的字 PresentationUI.footnote.tertiaryLabel,實測淺色 1.70:1、深色 2.23:1,兩個模式任何字級都不過 AA。用它畫的包括「查驗方一定會看到你的姓名」「這段文字是查驗者自己填的」——以及「『未通過』不等於偽造。裝置時間不準、條碼沒掃完、App 版本太舊,都會得到同樣的結果」
    修法:footnote.secondaryLabel(26% → 60% 墨水),另加 PresentationUI.mitigation:body 字級、全墨,給那句專用。跟持證方那句輪播秒數當初搬出 footnote 是同一個理由——寫給剛在別人面前看到紅畫面、只有幾秒可以說話的人看的句子,不該是全畫面最淡的字。
  • 已修 a-3 noPendingRequest 穿一樣的紅底,卻是唯一沒有那句緩解的 這個分支什麼都沒有檢查過,它說的完全不是對方出示了什麼,卻穿著跟真正拒絕一模一樣的紅底與「查驗未通過」,而且是唯一少了那句 footnote 的——讀者拿到指控卻拿不到但書。
    修法:改成橙色的「沒有東西被查驗」並補上緩解句(「這是這台手機的狀態,不是對出示者的判斷」)。規則同能力頁的 .partial不是壞狀態的狀態,不可以畫成壞狀態。 兩張結果畫面 render 成 PNG 逐一看過,現在一紅一橙、不再是同一張臉。

其餘 37 條尚未動工。以下是還沒修、但值得單獨提的幾條。

  • 待辦 b-1 能力頁的三個問句與那段「它實際上證明了什麼」,全文是英文 LocalizationCoverageTests 涵蓋三種 caveat 型別,就是沒涵蓋 PresentationScenario,所以全綠。那一頁存在的唯一理由是不讓 partial 被讀成 supported,而那句話對它要服務的讀者不可讀。 這個洞也擋著 M5.5——新增 TWDIW 的兩條 caveat 之前要先補覆蓋,否則新的英文字串會照樣全綠。
  • 待辦 c-3 查驗方斷線後,出示端永遠停在「傳送… 47%」 BluetoothLinkPeripheraldidUnsubscribeFrom。這是「有沒有哪個失敗完全沒有畫面」的正解,而且落在最容易被中斷的那條路上。跟先前修掉的 ack 競態是同一個家族。
  • 待辦 a-5 App 自己簽發的欄位,穿著「不可信第三方」的衣服 ClaimLabel 只認得五個 key,沒有 AgePredicate.claimName,所以「滿 18 歲」落到 .declaredByTheDocument,被穿上那套專門為了擋 zzz_official 才建立的引號樣式,值還顯示成 true。稀釋那個訊號——里長下次看到真正可疑的欄位會覺得「App 常常這樣」。而它正好是能力頁拿來當招牌的第一個場景。
  • 待辦 b-3 但書是灰的,次要記帳說明是黑的 「這次查驗沒有確認的事」那整塊每條訊息是 .secondaryLabel(實測淺色 3.44:1,一般文字不過 AA),而「對方另外保留了 2 個欄位」那顆藥丸是 .label(21:1)。位置守住了,同一塊內部的視覺重量沒守住,而且順序是反的。 只在淺色模式壞——而白天在櫃檯正是淺色。一行可改。
  • 待辦 a-1 綠勾說「這台裝置」,但讀那句話的人手上也有一支 判定行寫「持有這台裝置的人簽署了本次查驗」,而同一頁其他地方都把那支手機叫「對方手機」。只有唯一那行綠字用了會指向讀者自己的指示代名詞,而且落在螢幕上最大、最可能被讀完就停的一行——方向是往高估。
  • 待辦 a-6 時鐘對不上時,指控對方 訊息說「對方裝置的時間誤差太大」,但判斷用的是兩邊各自的時鐘讀數——查驗方自己慢三分鐘會得到一模一樣的句子。同一個 App 的鏡像情形(持證端)說的是「兩支裝置其中一支的時間不準」。避開了偽造的指控,換上另一個一樣沒有根據的指控,而被指的人無法反駁。
  • 研究 報告裡有一節叫「明確不建議做的」 存在的理由是:UI/UX 建議天然傾向「讓不好看的東西不礙眼」,而這個 App 的價值恰好在那些不好看的東西——攤開的 caveat、不能藏的姓名、說得出口的失敗。所以盤點自己要先列一節防這件事,跟 TWDIW 規劃裡那節「明確不做的」是同一個手法。

現有資產盤點

三個 iOS 專案、一個網站、一整套上游 ZK 元件。哪些接手、哪些當素材、哪些直接用。

資產 內容 最後更新 處置
backupTW-iOS UIKit,約 1,400 行。MyData 取件與 PDF 解析。無金鑰、無儲存、無 VC、無 ZK。 2026-01 主幹
bond-tw-app SwiftUI,24 個畫面:設計系統、徽章、資料主權、刪除流程、QR 掃描。 2025-08 UX 素材
Bondbear-vanilla SwiftUI + CoreData,完整 ZK 驗證流程 UX,但證明是模擬的(enum 驅動)。 2025-08 UX 素材
bond-website Astro 白皮書站(中英雙語)+ React 行銷站與繪本。 2026-03 沿用
openac-rsa-x509-swift iOS 16+ SPM 套件:本機產證、電路輸入組裝、離線 SMT 撤銷檢查。 持續更新 直接引用
...ios-example 端到端範例:TW FidO 驗簽 → 下載電路 → 產證明 → 送驗。已上 TestFlight。 持續更新 參考實作
moica-revocation-smt MOICA CRL → Poseidon SMT → 鏈上根值,附離線快照。 持續更新 直接引用
go-zkid-verifier Go 驗證器:challenge 簽發、證明驗證、nullifier 去重。線上架構 持續更新 需離線化改造
thematters/zkid-personhood 同一條路的先行者:已實測跑到 /link-verify 回傳 verified=true。附可用的 TW FidO 簽章 helper 與 e2e runbook。 2026-05 前案,直接借用

風險登記

風險 影響 對策
最小揭露只對查驗方成立,對內政部不成立 TW FidO 的 SIGN 請求同時帶著身分證統一編號(原始值,非雜湊)、sp_service_id 與依賴方識別碼。也就是「這個人、這個時間、為這個依賴方取得簽章材料」——內政部在證明存在之前就看得一清二楚 誠實的說法是揭露對象轉移而非消失:櫃檯什麼都不知道,發證機關什麼都知道。畫面若只寫「不會顯示你的身分證字號」而不寫對誰,對櫃檯為真、對內政部為假。已列為每一份證明都帶著的 idNumberDisclosedToIssuer caveat
signed_response 是不會過期的持有者令牌 簽章簽的是固定的 app_id,不是每次不同的挑戰值。任何拿到它的人——包含 SP 自己——都能無限期重複使用它產生證明。證明因此只能說「某位持卡人曾經取得簽章材料」,不能說「站在這裡的這個人」 已列為 signatureMaterialIsReplayable。真正的修法要上游把挑戰值綁進簽章範圍;在那之前不得有任何畫面把證明講成「本人在場」
測試 SP 憑證是共用範例值 開發無礙,但不能當成 bonds-tw 的正式身分;正式上線前仍須申請 視為開發用鷹架。專屬 SP 申請與組織 Apple 帳號同批送,別因為「能跑了」而忘記
開發機沒有 Xcode 且空間不足 iOS 端一步都動不了 先清 25 GiB 再裝 Xcode 26+;同時先跑不需要 Xcode 的 node 驗證步驟
macOS arm64 witnesscalc SIGSEGV 本機證明測試會誤判為 TW FidO 或憑證錯誤 Matters 已確認是 upstream 環境問題。證明穩定性一律在實機驗收
A2A 回呼網址可能需登記 簽完回不到 App,流程斷在最後一步 規格書未明訂。先試自訂 scheme,不行改 https:// 形式並向 MOICA 確認
離線驗證耗時過長 查驗現場不可用,5.3 的場景全部落空 先做實機量測 spike,再談 UX;必要時與 PSE 討論驗證端最佳化
離線無法全域去重 「真人且唯一」在緊急場景會被削弱 明確定義降級語意並寫進白皮書勘誤,不要讓文件承諾超出實作
MyData 無文件級簽章 憑證上的欄位事實只能靠持卡人背書。發證已改由持卡人的自然人憑證簽章(不再是裝置自簽),所以查驗方能確認「這位具名持卡人主張這些值」——但無法確認內政部核可過這些值,因為 MyData 的下載檔本身沒有任何簽章(2026-08-09 實測) 白皮書已列為已知限制;畫面上以「這位持卡人主張」而非「政府認證」表述;資料模型預留信任根替換位
卡片簽章路徑無法不可連結,而且洩漏的是姓名本身 查驗方必須拿到持卡人的 X.509 憑證才能驗簽,而該憑證的 Subject CN 就是法定姓名,另帶憑證序號與 RSA 公鑰。所以這不是「兩個查驗方能對出是同一人」,是「每一個查驗方都直接知道你是誰」。持有人就算不揭露姓名欄位也一樣——這一點畫面一度承諾得到而做不到,已於 2026-08-10 修正 結構性,改架構解不掉:X.509 沒有選擇性揭露,CA 的簽章覆蓋整份 TBSCertificate。唯一的解法是不送憑證、改在電路內證明憑證鏈——也就是 ZK 路徑,而 zkID 現有電路做不到(user_sig 簽的是固定 app_idcert_chain 不吃訊息)。目前的動作是把它明白寫在出示前與查驗結果兩個畫面上
上游 API 或格式變動 MyData 解析、TW FidO 流程隨時可能壞掉 CI 加 golden-file 測試;解析失敗要能優雅降級而非閃退
組織 Apple 帳號前置期 無法以 bonds-tw 名義發佈測試版 與 SP 申請同批送出