UrsaKit 製作與簽署 Apple Wallet 票卡指南的封面

Apple Wallet 票卡(.pkpass)是一個已簽章的 ZIP 壓縮檔,不是文件格式。Wallet 不會針對無效票卡顯示警告,而是直接拒絕加入,所以多數人第一次接觸這個格式,就是盯著一張怎麼都開不了的票卡。本文會從四個必要部分把票卡建起來,再說明那些會導致無聲退件的失敗模式。

Apple 也提供 Pass Designer,可用視覺化方式設計票卡,你可以用它先做版面原型。但當票卡要由伺服器產生時,你仍得自己組出這個壓縮檔。

01.pkpass 檔案實際上由什麼組成

把任何一張可用的票卡改名為 .zip 解開,你會看到這些部分:

檔案是否必要用途
pass.json是票卡定義:識別碼、樣式、顏色、條碼、欄位
manifest.json是套件內其他每個檔案的 SHA-1 雜湊
signature是對 manifest.json 的 PKCS#7 分離式簽章
icon.png、icon@2x.png是Wallet 與通知中顯示的小圖標
logo.png、strip.png、縮圖依樣式選用美術圖,尺寸必須符合 Apple 規範

manifest 是完整性檢查層,而且 Apple 的建置步驟刻意讓它保持單純:一個把每個來源檔的路徑對應到 SHA-1 十六進位摘要的扁平 JSON 物件。它永遠不包含自己或簽章,也不該把 .DS_Store 之類的中介資料檔列進去。

{
  "icon.png": "6c4f1b2a4e2b7d3c9a5f8e1d0b6a3c2e4f5a6b7c",
  "logo.png": "b1d2c3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0",
  "pass.json": "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"
}

manifest 必須與 ZIP 完全一致

如果 manifest.json 列出 logo@2x.png 但檔案不存在,或 ZIP 裡有檔案卻沒列進 manifest,簽章驗證就會失敗,Wallet 便會拒收。每次更動圖片後都要重新產生 manifest。

02為五種票卡樣式建立 pass.json

每張票卡都有一組共用欄位,然後宣告唯一一種樣式。共用欄位是 formatVersion(固定為 1)、passTypeIdentifier、serialNumber、teamIdentifier、organizationName 與 description。若你的伺服器要推送更新,還需要 webServiceURL 與 authenticationToken。

樣式字典只能是 generic、storeCard、coupon、eventTicket 或 boardingPass 其中之一。每種樣式提供不同的欄位區域:登機證有固定位置的 primaryFields、secondaryFields、auxiliaryFields 與 backFields,一般票卡則用相同區域承載不同用途。

欄位數量有上限。一般而言,一張票卡最多可有 3 個 header 欄位、1 個 primary 欄位、4 個 secondary 欄位與 4 個 auxiliary 欄位;登機證可用 2 個 primary 與 5 個 auxiliary,而優惠券、會員卡與方形條碼的一般票卡則共用 4 個 secondary 與 auxiliary 欄位。(PassKit 指南)

{
  "formatVersion": 1,
  "passTypeIdentifier": "pass.com.example.loyalty",
  "serialNumber": "member-8842",
  "teamIdentifier": "ABCDE12345",
  "organizationName": "Example Coffee",
  "description": "Example Coffee loyalty card",
  "backgroundColor": "rgb(67, 56, 202)",
  "foregroundColor": "rgb(255, 255, 255)",
  "labelColor": "rgb(224, 231, 255)",
  "storeCard": {
    "primaryFields": [
      { "key": "balance", "label": "Balance", "value": 12.5, "currencyCode": "USD" }
    ],
    "secondaryFields": [
      { "key": "tier", "label": "Tier", "value": "Gold" }
    ]
  },
  "barcodes": [
    { "format": "PKBarcodeFormatQR", "message": "member-8842", "messageEncoding": "iso-8859-1" }
  ]
}

有兩個細節特別容易出錯。顏色使用 rgb(r, g, b) 字串形式,不是十六進位、也不是陣列。而 serialNumber 是這張票卡的身分,若把不同會員重複使用同一個序號,推送更新時就會覆蓋到錯的卡片。

03產生 manifest 與 PKCS#7 簽章

簽章是無法省略的一步。你需要從 Apple 開發者帳號取得 Pass Type ID 憑證與其私鑰,再加上 Apple 的 WWDR 中繼憑證。

  1. 對除了 manifest 與 signature 之外的每個檔案計算雜湊。 在票卡目錄內執行:
for f in $(find . -type f ! -name 'manifest.json' ! -name 'signature' -maxdepth 1 | sed 's|^\./||'); do
  printf '  "%s": "%s",\n' "$f" "$(shasum -a 1 "$f" | cut -d' ' -f1)"
done
  1. 組成 manifest.json。 必須是有效 JSON,最後一項後面不能有逗號——上面的循環會刻意留下尾逗號,請自行移除,或改用腳本產生檔案。
  2. 對 manifest 簽章,而不是對個別檔案:
openssl smime -binary -sign \
  -certfile WWDR.pem \
  -signer passcert.pem \
  -inkey passkey.pem \
  -in manifest.json \
  -out signature \
  -outform DER
  1. 壓縮目錄,不要多一層父文件夾,也不要帶入 macOS 中介資料:
zip -r example.pkpass . -x '.*' -x '__MACOSX' -x '.*/'
  1. 出貨前先驗證。 簽章必須能對上你實際打包的 manifest:
openssl smime -verify -in signature -inform DER -content manifest.json -noverify

若這個指令印出 Verification successful,密碼學的部分就完成了。

04加入 Wallet 預期的圖片並設定尺寸

Wallet 會先縮放再裁切你提供的圖,所以真正要對準的是每張圖獲配的空間,單位是點。不符獲配空間的圖會被縮放後裁切,而 icon.png 是必填。

圖片獲配空間說明
icon.png29 × 29 點必填;鎖定畫面與郵件附件也用它
logo.png160 × 50 點通常會比獲配空間窄
strip.png375 × 123 點會員卡與優惠券為 375 × 144,活動門票為 375 × 98
thumbnail.png90 × 90 點長寬比維持在 2:3 至 3:2 之間
background.png180 × 220 點四邊會略為裁切並模糊

非 Retina 裝置上 1 點等於 1 像素,Retina 裝置上則是每個點 2 或 3 個像素,所以要一併提供 @2x 與 @3x 版本。(PassKit 指南)

最後才重新產生 manifest

先改圖片,再重建 manifest.json,接著簽章,最後壓縮。順序顛倒會讓 manifest 留下過期雜湊,做出看起來完成、實際驗證失敗的票卡。

05會無聲失敗的條碼規則

條碼是票卡從設計變成實用的分界。條碼掃描器與相關軟體通常預期 ISO 8859-1(Latin-1)或 Windows-1252,而大多數系統對 Unicode 支援很差,所以 message 裡出現 emoji 或非拉丁字元,就是最典型的無聲失敗。請讓條碼內容維持 ASCII,把給人看的文字放進欄位。

現象原因解法
票卡加得進去但條碼空白barcodes 陣列缺少或為空至少加入一筆有效 format
條碼顯示亂碼message 不是 ISO 8859-1 或 Windows-1252message 用 ASCII,文字移到欄位
Apple Watch 上沒有條碼只放了 Code 128,而 watchOS 不支援補一筆 QR 或 PDF 417 作為後備
舊 iPhone 顯示不出條碼只有舊式 barcode 鍵一併輸出 barcodes;iOS 9 起改用陣列

Wallet 支援 QR、PDF 417、Aztec、Code 128、Code 39、Codabar、EAN 13 與 ITF,PassKit 會採用陣列中第一個支援的項目。QR、PDF 417、Aztec 與 Code 128 在 iOS 9 或之後都有效,這四種不需要後備。手機畫面能給的面積很小,QR 是最安全的選擇。(條碼、PassKit 指南)

payload 越短越穩

每種格式都有硬性上限,而真實掃描環境會再把上限壓低——指南舉的例子正是在光線不足的環境下,用便宜的掃描器讀 Apple Watch 上的條碼。請用場地實際使用的硬體測試。

06先在實機測試,再規劃更新

Apple 記載的測試方式是把票卡拖進模擬器中的 iPhone,票卡有效時會出現加入對話框;對外推出前仍要用實機跑完整個加入 Wallet 流程。用 AirDrop 傳檔,或從 HTTPS 端點以 application/vnd.apple.pkpass MIME 類型提供,Safari 才會出現加入按鈕。(Technical Note TN2302)

若票卡在核發後還會變動——例如餘額、登機門、登機時間——你不會在裝置上重建它。你要在 pass.json 加上 webServiceURL 與 authenticationToken,裝置會向你的伺服器註冊並提供推送權杖,之後你的伺服器發送推送通知,裝置就會取回更新後的票卡。這也是 authenticationToken 必須又長又隨機的原因:它是驗證每一次更新端點呼叫的共用密鑰。

07常見錯誤與修正

錯誤可能原因修正
只顯示「無法加入票卡」manifest 與簽章不符重建 manifest、重新簽章、重新壓縮
簽章通過但 Wallet 拒收passTypeIdentifier 與憑證不符使用入口網站上完全一致的 Pass Type ID
票卡沒有套用樣式背景/前景色用了十六進位字串改用 rgb(r, g, b) 字串
更新永遠不送達serialNumber 錯誤或推送權杖過期每張票卡使用唯一且穩定的序號

幾乎所有失敗都指向同一個模式:Wallet 把票卡視為已簽章的產物,所以任何位元組變動——包括重新存一次圖片——都會讓簽章失效。寫一個一次完成「重建 manifest、簽章、壓縮」的腳本,你就不必再手動追雜湊。

08資料來源

常見問題

為什麼我的 .pkpass 加不進 Apple Wallet?

十次有九次是簽章與 manifest 對不上,或 manifest 列了 ZIP 裡不存在的檔案。這兩種情況 Wallet 都會默默拒收。

沒有 Apple 開發者帳號可以建立 Wallet 票卡嗎?

不行。簽章需要 Apple 開發者帳號核發的 Pass Type ID 憑證。你可以先做 JSON 與圖片,但 Wallet 不會接受未簽章的票卡。

manifest 用的是 SHA-1 還是 SHA-256?

manifest 格式是對套件內每個檔案計算 SHA-1,這與 Git 物件雜湊該用 SHA-1 還是 SHA-256 的討論無關。

為什麼條碼在 Apple Watch 上不見了?

watchOS 不支援 Code 128。如果 barcodes 陣列裡只有 Code 128,手錶上就完全不會顯示條碼,請補一筆 QR 或 PDF 417 作為後備。

為什麼我的條碼掃不出來?

通常是內容與掃描器的預期不符:條碼硬體一般讀 ISO 8859-1 或 Windows-1252,而系統對 Unicode 支援很差。請讓 message 維持 ASCII,並把給人看的文字移到欄位。

返回文章列表

相關文章

相關工具