目前實作狀態 · 2026-09-08 更新
底下的實作筆記仍是一份分級標示證據的紀錄;這裡只放皮夾現在真的做到哪。完整施工紀錄在工程藍圖。
- 已完成 · 領卡
- 真實政府卡端到端領下來了:交通部公路局的駕照電子卡走完整 OID4VCI 入庫顯示。
token400 的關卡已解——眉角是端點以client_id=moda_dw成對查pre-authorized_code;送 app 自己的 bundle id 會not found。 - 已完成 · 出示
- M5.4 OID4VP 出示端到端通過:同一張真卡選擇性揭露,先對官方 demo 驗證方通過,再對本專案獨立自建的 verifier(
verifier.mashbean.net,Cloudflare Workers)跨裝置驗證通過。 - 已完成 · 自建發卡端
- 姊妹專案「請收下卡片」已上線:
issuer.mashbean.net(Cloudflare Workers)發行 TWDIW 方言的測試卡,供皮夾領卡與出示回驗;卡片皆為虛構資料、不在官方信任清單上。有備而來 DEBUG 建置以沙盒例外釘住其did:key,Release 不含。 - 下一個閘門
- 資料保險箱:MyData 七份文件(所得/勞保/健保/納稅/勞退/地籍/戶籍謄本)路徑已配好、可在 app 內直抓,非身分證文件的欄位解析待見到真實資料逐份寫。
- 需要決定
- 任何表單提交或條款接受,都由使用者決定後才進行。
零、這一頁怎麼讀
底下每一條主張都掛了證據等級。這件事在這裡比平常重要,因為其中幾條是對一個政府系統的缺陷回報, 而一條經不起「你們實測過嗎」的主張,會連累其他站得住的。
- 實測 對公開的正式端點做唯讀請求 指令附在旁邊,任何人可以重跑。日期一律標出來,因為這些端點會變。
-
讀碼
從 MIT 授權的原始碼讀出來的
moda-gov-tw/TWDIW-official-app,2026-08-16 讀HEAD(最後 push 2026-07-27)。⚠️ 讀公開的原始碼不等於知道部署上去的 build 在做什麼。 凡是主張講的是線上行為、而證據只到原始碼的,我都會註明。 - n=1 單一樣本,或二手來源 記下來是因為知道它有用,不是因為它撐得起結論。這些不要拿去回報上游,除非先補樣本。
這些筆記是怎麼來的:唯讀量測與讀碼,寫的當下我們還沒領過任何一張卡。 底下沒有一件事需要發出一張憑證才做得到——這是它們的證據來源,也是它們的限度。 但自筆記寫成之後,情況已經往前走了:真實駕照電子卡已完整領下來、也已出示並被驗證方 端到端驗過(見上方實作狀態)。所以請把底下各節讀成「當時對公開端點與原始碼量到的事實」, 而不是專案現在的邊界。
一、第三方皮夾這個角色不存在
不是「很難」。是文件、沙盒、資料模型裡都沒有這個角色。
| 角色 | 要做什麼才能上場 |
|---|---|
| 發行端 | 申請沙盒帳號 → 自架 → 跟團隊做 UAT → 正式 |
| 驗證端 | 同上,換成驗證端那一套 |
| 皮夾(持有端) | 文件沒有寫。沒有申請表、沒有註冊、沒有 attestation。 |
沙盒帳號申請頁的對象是「產品、服務或程式之串接測試」,指向
issuer-sandbox 與 verifier-sandbox。沒有 wallet-sandbox。
而在《Missing Manual》裡,標題叫
「If You're Building a Third-Party Issuer or Verifier Software」的那一節是空的,
第三方皮夾連標題都沒有。
-
讀碼
client_id不是名單,是回音 發行端自己簽一個 ID Token,aud是寫死的字串"moda_dw"(twdiw-vc-handler/…/CredentialService.java)。OID4VCI handler 接著 從那個aud讀出client_id(twdiw-oid4vci-handler/…/CredentialIssuer.java),存進 pre-authorized code。 到了/token,皮夾送的client_id拿去跟那筆比對; proof JWT 的iss再跟同一個值比對一次。
整條鏈驗的是「你送的字串等於你送的字串」。 資料庫 49 張表裡沒有任何一張是 client、wallet、app 或 device 的註冊表。 沒有client_secret、沒有 client assertion、沒有 mTLS、 沒有 Play Integrity、沒有 App Attest、沒有 DeviceCheck。
同一次閱讀還撞到兩件事:ID Token 的簽章被取出來之後從此沒有被使用;AccessTokenFilter的驗證邏輯整段被註解掉,旁邊留著 「尚未啟用 AccessToken 驗證」,而/api/**是permitAll。 -
小心
這件事的意思,以及不是什麼意思
這不表示任何人都能領到憑證。領卡真正的門檻是 pre-authorized code
——也就是那張 QR——加上可選的
tx_code。站得住而且更有力的說法是:
拿得到那張 QR 的人,用任何自製皮夾都能走完全程; 官方 App 沒有任何密碼學上或註冊上的特權。
還有一個讀原始碼消不掉的限制:我們沒有對正式的領卡端點送過任何一個請求, 所以無法排除網路層(WAF、API gateway、mTLS、IP 限制)另有管制, 而那些東西不會出現在 repo 裡。
二、did:key 的拼法不是你實作的那一種
皮夾最先死在這裡,而且是以一個離真正原因兩層遠的解析錯誤死掉的。
數位憑證皮夾用的是 multicodec 0xEB51(jwk_jcs-pub),
payload 是整份 JWK 的 JSON 文字。不是 p256-pub(0x1200)
——後者帶的是 33 bytes 的壓縮點,而那是多數 did:key 實作對 P-256 產生的東西。
拿一筆真實的信任清單條目逐位元組解開:
129 bytes
前 3 bytes = D1 D6 03 # 0xEB51 的 LEB128 varint
其餘 126 = {"crv":"P-256","kty":"EC","x":"kY6ina…","y":"c5je-…"}
伺服器端把那三個位元組寫死切掉,multibase 字元不檢查、multicodec 也不檢查:
String did_hex_prefix = CryptoHelper.convertBinaryToHexString(Base58.decode(did.substring(1)));
String did_hex = did_hex_prefix.substring(6); // 去掉prefix D1D603
String did_jwk = new String(CryptoHelper.hexStringToByteArray(did_hex));
所以你送一個 did:key:zDn… 過去,它會少掉三個位元組、然後被當成 JSON 解析。
你拿到的是一個 JSON 錯誤,而真正的原因——multicodec 不對——沒有任何地方會提到。
-
實測
正式環境每一個發行者 DID 都是 JCS 正規的
正式信任清單 43 筆全部:multicodec
0xEB51×43、JWK 鍵序crv,kty,x,y×43、自簽章驗得過 ×43、內嵌 DID 文件的id相符 ×43、 header 的jwk與 DID 內嵌金鑰相符 ×43。 發行者那一側是乾淨的。 -
n=1
而官方皮夾自己的 holder DID 不合規
jwk_jcs-pub的定義就是 JWK 經過 RFC 8785 的 JCS,鍵序必須crv<kty<x<y。 我們手上那一個 holder DID 是crv, x, y, kty——kty掉到最後。
⚠️ 一個樣本,來自文件而不是我們自己跑出來的皮夾,而且沒有記錄 App 版本。 這足以決定解析器要怎麼寫,但不足以當缺陷回報。要回報得先從一個標明版本的 build 蒐集幾個 DID。 -
規則
產生要嚴格,接受要寬鬆——而且永遠不要比較原始的 DID 字串
自己產生的一律正規化。不要要求接受的東西正規:
嚴格的解析器會拒絕真實世界唯一存在的那個台灣皮夾,而金鑰的位元組兩邊都不含糊。
寬鬆會打開一個真的問題,因為下游一律拿 DID 當字串比對——JWS 的kid對issuer、出示者的 DID 對credentialSubject.id。 關掉它的方式是:比較正規形式,也就是從金鑰算出來的那一份, 而不是比較收到的字串。兩個 DID 指同一把金鑰,恰好等價於它們的正規形式相同, 沒有任何拼法躲得掉。
測試向量。兩個都是真的,來自正式環境:
# 行政院-數位發展部(正規——必須逐字元 round-trip 回它自己)
did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrzifm9txeerMVc9oLUg2nBJJnUtgYcAYd35rw1rCLq8y3bLDDBUPH5yTYB7ocY7oPESPBXqubuwMcRzw9evbeHHyFkwsmDc43myibDChGhDk8zrgZDB4KNyXPiQvkktUwn
# 一個皮夾的 holder DID(非正規——但必須解得開)
did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPodrZSqMbCy9Ndu4UgUGy3RNkhH479eLPpbfAhVSNu7B4oJvUwLzyxiP4Jt5k9cqqmChanxAazTGxJMvGxYDApNkXeDW5MPZgZRkjRgD1yaig5KCEgAaVbg8zrvYjMTi1BzqdDpPpkeSFmJwiej9YNY
三、憑證是 SD-JWT,而沒有任何欄位會告訴你
這是最貴的一個坑,因為寫錯的那個版本會通過你想得到的每一個測試。
-
實測
metadata 說
jwt_vc_json,而且是全部GET https://issuer-oid4vci.wallet.gov.tw/api/issuer/00000000/.well-known/openid-credential-issuer→ 701,000 bytes、882 組 credential configuration。format分布:{jwt_vc_json: 882}。整份文件裡sd-jwt出現 0 次、_sd0 次、selective與disclosure各 0 次。
實際回來的是typ: vc+sd-jwt加一串~分隔的揭露。 一個照規範讀 metadata 的 OID4VCI 客戶端,沒有任何欄位可以讓它發現這件事。~是唯一的訊號。
(順帶一個營運面的數字:為了拿一組設定要下載 701 KB。抓一次就好。) -
實測
摘要算在 base64url 字串本身,不是解碼後的 JSON
兩個讀法都說得通,只有一個是對的。選錯的後果是你的皮夾對每一份誠實的揭露都找不到匹配,
然後把每一張真卡回報成偽造。
沒有照規範推,直接拿正式資料驗:2025-10-07 一張真實駕照電子卡的六段揭露, 以及同一張憑證公開的兩個_sd值。base64url(SHA-256(揭露自己的 base64url 字串,ASCII))兩個都對得上。對解碼後的位元組做雜湊則一個都對不上。
自己跑一次:
python3 - <<'EOF'
import hashlib, base64
b64u_e = lambda b: base64.urlsafe_b64encode(b).decode().rstrip('=')
D = "WyJwbHowWFN6LW9CSEUwZTUzTFVBeWNBIiwiaWRfbnVtYmVyIiwiQTIzNDU2Nzg5MCJd"
print(b64u_e(hashlib.sha256(D.encode('ascii')).digest()))
# ApkeYAR85EzxAHS1ojnNHhG7wnCDyTt4_iCIX2VKxaw ← 憑證裡公開的那個 _sd 值
EOF
-
讀碼
它是混血:SD-JWT 裝在 VCDM 1.1 的
vc裡_sd與_sd_alg在vc.credentialSubject底下,不在頂層。 型別是vc.type[1],不是vct。所以照 SD-JWT VC 寫的讀取器 會在錯的地方找,照 VCDM 寫的則根本不會把揭露切出來。兩種都不拋錯,兩種都是錯的。
發行端與後端驗證端用的都是com.authlete:sd-jwt,所以摘要語意是對的 ——只是那份 metadata 從頭到尾不承認這個格式存在。 -
實測
statusListIndex是字串"statusListIndex": "35",不是35。只接受數字的讀取器會把整個credentialStatus物件丟掉——而一張沒有 status 的憑證, 就是一張永遠不會被查撤銷的憑證。靜默,而且往看起來安全的方向失敗。
四、信任清單
比想像中小,錨定得比想像中好,而它的分類標籤絕對不能顯示給使用者。
-
實測
正式環境 43 筆,而
size不被遵守GET https://frontend.wallet.gov.tw/api/did?size=20&page=0&orgType=1&status=1orgType=120 筆、orgType=223 筆、3 與 4 是 0。 用兩種頁大小(20 與 5)各自逐頁抓到空頁,兩次總數相同,識別碼全部不重複。
⚠️ 頁大小被夾在 20,但 offset 看起來是照你送的size算的, 所以size=100&page=1什麼都不會回。一頁一頁抓到空為止, 不要自己用size算 offset。
43 筆裝得進 App。 這件事改變了一個離線皮夾可以做什麼。 -
實測
不要顯示
orgGroupDetail.name43 筆全部標成「政府部門」。這 43 筆裡面包括全家便利商店、統一超商、中華電信、 台灣大哥大、遠傳電信,以及一批民間公司。
任何照字面顯示這個欄位的介面,都會告訴使用者全家便利商店是政府部門。 真正區分角色的看起來是orgType1/2——公路局兩邊都有,超商只在 2, 而超商正是電信憑證那個「超商取貨」情境的驗證方。 -
實測
鏈上存的是完整文件,不是雜湊
Arbitrum One,合約
0x84172caf8dd126c76f1fa8a2733ca3233264d31f。 數發部那一筆錨定交易的 calldata 有 2,596 bytes,內容是完整的 DID 字串與 完整的自簽 DID 文件 JWS 的明文。sha256(DID)不在裡面。
這比「刻在石碑上」那句宣傳更強:資料本身在鏈上,所以客戶端原則上可以 完全不信任frontend.wallet.gov.tw。
⚠️ 但我們只驗了一筆交易,而且那筆的 tx hash 是那個 API 給的。 要證明這是一條真的可用的無信任路徑,得示範「只給合約位址就能列舉全部 43 筆」。沒有做。 -
n=1
43 筆裡有 41 筆上鏈——那兩個例外值得知道
中國醫藥大學出現兩次(兩個
orgType各一),onChainHistory是空的、x509_type是XCA, 而且雖然登記在發行端那個orgType底下,issuerMetadataBaseURL卻是null。
⚠️ 這是那個 API 自己的自述。我們沒有去鏈上找它們, 所以「沒上鏈」在這裡的意思是「API 說沒有」——考慮到錨定本來就是為了不必相信那個 API, 這是循環的。
五、撤銷這條路沒有離線的信任錨點
憑證那條路錨在公鏈上。撤銷這條路只有一個 URL。
-
實測
簽撤銷清單的金鑰不在發行者的 DID 裡
抓一份撤銷清單,用兩種方式各驗一次:
• 用它自己iss的did:key內嵌金鑰 → 驗不過
• 用jku那組的key-2→ 驗得過GET https://issuer-vc.wallet.gov.tw/api/keys回兩把。key-1就是發行者 DID 內嵌的那把,簽憑證;key-2簽撤銷清單,而它不在任何 DID 文件裡、不在信任清單裡、 不在任何鏈上紀錄裡。那份 DID 文件只有一個 verification method。
所以:查一張憑證有沒有被撤銷,信任基礎只有 TLS。 把清單快取起來也沒用——離線一樣驗不了它的簽章。 -
實測
有效期是「上次被簽之後 24 小時」,不是「抓下來之後」
nbf到exp剛好 86,400 秒。但nbf是 這份清單上一次被簽的時間,不是你抓下來的時間。
所以一份剛下載好的撤銷清單,剩餘效期介於 24 小時與趨近於零之間。 介面上要顯示清單自己帶的exp,永遠不要跟使用者說「24 小時內有效」。
清單本身很便宜:壓縮後 76 字元、解壓 16,384 bytes、容量 131,072 bits, 量測當下有 84 個位元是 1。常常抓是可行的,問題只在離線的那一段。
⚠️ 重簽節奏是 n=2(一個 2025 年的樣本是發卡時重簽, 一個 2026 年的落在每日邊界)。不要拿那個時刻去設計預抓排程; 關於剩餘效期的結論不受影響,因為它只需要exp是絕對時間。
六、證明年齡會交出生日,而卡片本身會出賣自己
兩張完整的駕照電子卡,端到端驗過。分隔「已滿 18」與「未滿 18」的只有一個揭露 ——而沒有任何辦法在不交出生日的情況下送出它。
-
實測
兩張都驗得過,而且只差一個欄位
取得兩張完整的 SD-JWT 並逐項檢查:六個揭露全部重算得出它們的
_sd摘要 (兩張各 6/6),兩張的 ES256 簽章都用線上抓下來的key-1驗得過。
兩張結構完全相同。name、id_number、type、controlnumber、gDate五項的值在兩張裡一樣; 只有roc_birthday不同——1040605(民國 104 → 2015-06-05,未成年) 對0570605(民國 057 → 1968-06-05,成年)。鹽不同所以摘要不同, 其餘就是同一份文件。 -
實測
沒有年齡述詞——要證明滿 18 就得交出生日
沒有 range proof、沒有
age_over_18這種布林欄位、沒有任何衍生值。 用這張憑證回答「你滿 18 了嗎」的唯一辦法,是把roc_birthday完整揭露,精確到日。
那正是超商那個最普通的情境,也正是選擇性揭露最常被拿來當賣點的情境。 機制本身是在的而且是對的——SD-JWT、加鹽、逐欄位——而它仍然沒辦法在不交出 一個把持有人縮得比問題所需窄得多的日期的前提下,回答這個問題。 -
實測
你扣住了幾個欄位是看得見的,是哪幾個則猜得到
_sd剛好六個摘要,而定義那六個欄位的 credential configuration 公開在 issuer metadata 裡誰都讀得到。所以一個收到四個揭露的查驗方, 知道有兩個被扣住,也知道它們是從哪一份六項的清單裡挑的。
扣住仍然值得做——值本身是藏住的——但它不是隱形的, 而一個把它講成「他們不會知道」的皮夾是說過頭了。 -
實測
每張卡都帶著一個唯一索引,明文,每次出示都送
credentialStatus.statusListIndex是逐張憑證的:這兩張一張是#20、一張是#19。它不可選擇性揭露 ——它在簽過的 payload 裡、在_sd之外,所以每一次、對每一個查驗方都會送出去。
兩個查驗方對一下就知道他們看過同一張卡,不需要任何其他欄位。 這是 StatusList2021 的本質而不是 TWDIW 的失誤——索引正是撤銷之所以查得到的原因。 會寫出來是因為一個皮夾的選擇性揭露介面,很容易讓人以為情況相反。
七、皮夾裡的卡,離線時沒有名字
把卡片列做出來、render 出來、看了一眼才發現的。那一列長這樣:
00000000_demo_drivinglicense_202504251418。
-
實測
憑證裡唯一能拿來稱呼它的欄位是一個機器識別碼
vc.type[1]是皮夾唯一叫得出它名字的欄位,而它是發行系統的內部識別碼: 四十一個字元,在 390pt 寬的螢幕上折成兩行。人看得懂的名稱在發行者 metadata 的credential_configurations_supported[…].display[].name—— 那是一個 HTTPS 端點(實測 701 KB、882 組設定)。
所以一個離線的皮夾,無法告訴持有人他手上這張卡叫什麼。 站在櫃檯前,他看到的是一串底線和數字,要自己認出哪一張是駕照。 - 實測 而且沒有任何簽章覆蓋那個名稱,所以快取解決不了 這跟撤銷不一樣。撤銷至少還有一個講得清楚的降級(「離線超過 24 小時就不可知」)。 名稱抓一次快取起來,那份快取不是憑證的一部分——沒有東西簽它—— 所以顯示快取標籤的皮夾,顯示的是它自己的筆記,不是發行者的宣稱。 離線要拿到一個可信的卡片名稱,這條路根本不存在。
-
程式碼
我們怎麼處理,以及刻意不做什麼
CardInventory.readableType拿掉兩段對持有人毫無意義的東西—— 開頭全是數字的發行者代碼,結尾 8–14 位數字的核發時間戳——其餘原樣輸出。 上面那一列因此變成demo_drivinglicense。
刻意不做對照表。一張「已知型別→中文名稱」的表, 對表上沒有的型別就是一個猜測,而沒在表上的型別正是猜錯最要命的地方。 兩端的裁切都是條件式的,裁完會變空字串就原樣輸出。
這改善了可讀性,但沒有解決問題:demo_drivinglicense依然是發行系統的字,不是持有人的話。真正的解法在發行端,所以它是下面的第 8 件。
八、八件值得回報上游的事
依使用者受害程度排,不是依技術嚴重度。第 6、7 件因為證據不足而暫緩, 列在這裡是為了讓暫緩的理由看得見,而不是默默不提。
-
讀碼
一、選擇性揭露可能送出使用者沒有勾選的欄位
皮夾 SDK 決定保留哪些揭露的方法,是拿要求的欄位名去對整段解碼後的 JSON 做
子字串比對——而那段 JSON 包含鹽與值:
String decoded = utf8.decode(base64.decode(base64Url)); if (decoded.contains(field)) { … }
駕照同時有id_number與controlnumber,兩個都含子字串number。值裡若出現另一個欄位的名稱也會誤中;隨機的鹽理論上也可能命中。 另外那個continue在內層迴圈,所以一個揭露同時命中兩個欄位會被附加兩次。
使用者勾選「只揭露 A」,可能送出 A 與 B。 這是這個功能的核心承諾,也是這一頁唯一一條不需要解釋為什麼有害的缺陷。
⚠️ 只有原始碼。沒有記錄 App 版本,也沒有在流量上觀察到。 -
讀碼
二、四個對外抓取關閉了 TLS 主機名驗證
發行者公鑰、撤銷清單、schema、以及 frontend 的 DID 查詢,四個都傳
setAllowedHostnames(null)。而在HttpUtils裡那不是 「不設清單、走系統預設」——自訂的HostnameVerifier無條件回傳true。那四條連線上, TLS 憑證與主機名之間的繫結消失了。
RFC 8725 §3.8 明文要求jku必須限制在受信任 URL 的允許清單。
⚠️ 只有原始碼;而且這描述的是公開的驗證端程式,由各驗證機關自行部署, 我們無法確認任何一個實際部署跑的是什麼。 -
讀碼
三、撤銷清單與 schema 在發行者簽章驗證完成前就被抓取
validateVC()把六個步驟當成同時起飛的 future:抓撤銷清單(第 3 步) 與抓 schema(第 5 步)都比驗發行者簽章(第 6 步)早,而那兩個 URL 取自 尚未驗簽的 payload。配上第二條,任何能對驗證端 POST 一份 VP 的人, 都能讓它對任意 URL 發出 GET。
另外StatusListCheckTask沒有 DID 優先那一段, 直接走loadIssuerPublicKey(jku, kid)。這是jku這個模式在 正式環境真正活著的那個實例,而它是自我指涉的:清單自己說用哪把鑰匙驗自己。 -
讀碼
四、
client_id不帶任何資訊,而且兩道檢查是關的 見第一節。ID Token 的簽章從未被驗;AccessTokenFilter的驗證被註解掉。 -
實測
五、
format沒有描述憑證 882 組全部。見第三節。 -
讀碼
六、VP 物件用
context而不是@context皮夾 SDK 裡三個產生 VP 的函式全部寫'context',少了 at 符號。 搜尋 VCDM 的 context URL 只找到那三處,也就是說 系統裡沒有任何一條路徑寫對。
⚠️ 我們一開始把它描述成「JSON-LD 處理會靜默丟掉主張」。那是錯的 ——TWDIW 的驗證端根本沒有做 JSON-LD 展開,所以今天沒有東西被丟掉。 準確的說法是:這是一個會咬到任何有做展開的第三方驗證端的互通性缺陷, 不是一個正在造成資料遺失的 bug。值得回報,但不值得用我們一開始那個寫法回報。 - n=1 七、holder DID 不符合 JCS——暫緩 見第二節。一個樣本、二手、沒有 App 版本。在拿到一個標明 build 的幾個 DID 之前不回報。 列出來是為了讓這個缺口看得見。
-
實測
八、憑證沒有攜帶任何離線可讀的名稱
見第七節。
vc.type[1]是內部識別碼;人類可讀名稱只存在於線上 metadata, 而且不在任何簽章的覆蓋範圍內。
建議的修法:把 display name 放進 VC 本身——例如vc.name,或 SD-JWT VC 的vct搭配一份有簽章覆蓋的型別 metadata——讓離線皮夾能顯示持有人認得的名稱。
受害程度排序上它不高,沒有人因此被冒用。但它是這張清單上唯一 每一位持有人、每一次打開皮夾都會遇到的,而其他多半不會。 -
設計
還有一件不算缺陷的:發行者換不了金鑰
發行者的身分是一個
did:key——金鑰就是身分。 所以輪替簽章金鑰等於變成另一個發行者,而所有已經發出去的憑證的iss從此不在信任清單裡。皮夾將無法區分 「這個發行者被撤下了」與「它換了金鑰、舊卡其實還是真的」。
這是這一頁唯一一件會讓已經在使用者手上的卡集體變成無法評估的事。
九、我們沒有驗證過什麼
列出來是因為一頁只有發現而沒有這一節,等於在邀請別人過度解讀。
- 部分領卡跑到 token,還沒領到卡2026-08-26 實測:從
demo.wallet.gov.tw產生駕照卡 offer,App 完整走到 token 步驟——URLSession過了 Cloudflare、issuer 收下請求並回以業務錯誤,卡在pre-authorized_code not found(HTTP 400)。已排除 TTL(curl 毫秒級連續 GET→token 同樣 not found);最可能是 offer 與 token 分屬不同環境(deeplink 走 UAT,offer_uri 指 production-ish issuer)。因此「送一個不是moda_dw的client_id會不會被拒」仍未判定——它被擋在 pre-auth code 之前。這不是皮夾端的 bug,collector 走完了整條鏈。 - 缺口原始碼不等於部署第一到第四與第六件都是從公開 repo 讀出來的。我們無法確認部署上去的 build 與它一致。
- 缺口一組設定、一個時間點撤銷清單的量測是單一設定在單一時刻。另外 881 組沒有取樣。
- 缺口「欄位值都是字串」駕照那六個欄位都是字串。2026-08-26 讀 issuer metadata 確認這三張卡存在:
universal_telecom_card「萬用電信卡」帶phone、convenience_store「便利商店取貨卡」帶name+Phone_number_last3(超商取貨核銷比對的兩欄,欄名大小寫混合)。但這幾張的值型別仍不知道——它們不在 demo 訪客領卡的下拉裡(只有駕照、訪客、所得、學生證、畢業證書),要走官方 App 的門號/帳號驗證才領得到。失敗形態很差:嚴格要求三元素字串陣列的讀取器會拒收整張卡,於是一張真卡被判成壞卡。