Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPerにこそ勧めたいAppleウォレット / Google ウォレット開発

Avatar for muno92 muno92 PRO
October 03, 2026

PHPerにこそ勧めたいAppleウォレット / Google ウォレット開発

Avatar for muno92

muno92 PRO

October 03, 2026

More Decks by muno92

Other Decks in Technology

Transcript

  1. 自己紹介 • X (旧Twitter): @muno̲92 • 趣味:カンファレンススタッフ • PHPerKaigi •

    PHPカンファレンス小田原 • forteeのウォレット連携機能を作ってます 2
  2. 話すこと 話さないこと • ウォレットの基本的な概念 • 異常系でのデバッグ方法 • パスの新規追加 / 更新の流れ

    • 正常系only • (スライドの末尾に付録として載 せています) • パスの種類毎の細かい実装方法 14
  3. JSONや画像などを1つのzipにまとめている > zipinfo phpconehime-2026.pkpass Archive: phpconehime-2026.pkpass Zip file size: 838755

    bytes, number of entries: 6 -rw-r r 6.3 unx 153237 b- defX 25-Dec-08 20 17 logo.png -rw-r r 6.3 unx 153237 b- defX 25-Dec-08 20 17 icon.png -rw-r r 6.3 unx 560360 b- defX 25-Dec-10 01 08 strip.png -rw-rw-rw- 6.3 unx 1245 b- defX 26-Sep-10 23 34 pass.json -rw-rw-rw- 6.3 unx 219 b- defX 26-Sep-10 23 34 manifest.json -rw-r 6.3 unx 3386 b- defX 26-Sep-10 23 34 signature r : : : : : : - - - - - - - - - - - - - - - - 19
  4. パスクラス (例) { "id": "パスクラスの識別子", "issuerName": "PHPカンファレンス愛媛2026", "reviewStatus": "UNDER_REVIEW", "eventName":

    { "defaultValue": {"language": "ja-JP", "value": "本編"} }, "hexBackgroundColor": "背景色", "logo": { "sourceUri": {"uri": "ロゴ画像のURL"} }, "heroImage": { "sourceUri": {"uri": "バナー画像のURL"} } }, "classTemplateInfo": { "cardTemplateOverride": { "cardRowTemplateInfos": [ { "twoItems": { "startItem": { "firstValue": { "fields": [{"fieldPath": "object.ticketNumber"}] } }, "endItem": { "firstValue": { "fields": [{"fieldPath": "object.textModuleData['name']"}] } } } } ] } } 22
  5. パスオブジェクト (例) { "id": "オブジェクトID", "classId": "紐づくパスクラスのID", "state": "ACTIVE", "textModuleData":

    [ { "id": "name", "header": "参加者名", "body": "muno92" } ], "ticketNumber": "X-7", "barcode": { "type": "QR_CODE", "value": "QRコードに埋め込む文字列" } } 23
  6. パスクラスにパスオブジェクトを埋め込むと { "id": "オブジェクトID", "classId": "紐づくパスクラスのID", "state": "ACTIVE", "textModuleData": [

    { "id": "name", "header": "参加者名", "body": "muno92" } ], "ticketNumber": "X-7", "barcode": { "type": "QR_CODE", "value": "QRコードに埋め込む文字列" } } 26
  7. 再掲) pkpassにはJSONなどが含まれている > zipinfo phpconehime-2026.pkpass Archive: phpconehime-2026.pkpass Zip file size:

    838755 bytes, number of entries: 6 -rw-r r 6.3 unx 153237 b- defX 25-Dec-08 20 17 logo.png -rw-r r 6.3 unx 153237 b- defX 25-Dec-08 20 17 icon.png -rw-r r 6.3 unx 560360 b- defX 25-Dec-10 01 08 strip.png -rw-rw-rw- 6.3 unx 1245 b- defX 26-Sep-10 23 34 pass.json -rw-rw-rw- 6.3 unx 219 b- defX 26-Sep-10 23 34 manifest.json -rw-r 6.3 unx 3386 b- defX 26-Sep-10 23 34 signature r : : : : : : - - - - - - - - - - - - - - - - 30
  8. (例) pkpassに含まれるファイル • pass.json パス情報が記載されたメインのファイル • logo.png • icon.png 画像ファイル

    • strip.png • manifest.json 改ざん検知用のハッシュが記載されたファイル • signature 34
  9. (例) pkpassに含まれるファイル • pass.json パス情報が記載されたメインのファイル • logo.png • icon.png 画像ファイル

    • strip.png • manifest.json 改ざん検知用のハッシュが記載されたファイル • signature 発行者検証用の署名ファイル 35
  10. pass.json • 画面に表示される値やOSが読み取るメタデータを格納 { "formatVersion": 1, "passTypeIdentifier": "pass.com.example.muno92", "serialNumber": "パス毎に一意なID",

    "teamIdentifier": "XXX", "organizationName": "PHPカンファレンス愛媛2026", "description": "PHPカンファレンス愛媛2026 本編・懇親会", "logoText": "PHPカンファレンス愛媛2026", "barcode": { "message": "QRコードに埋め込む文字列", "format": "PKBarcodeFormatQR", "messageEncoding": "iso-8859-1" }, "eventTicket": { "headerFields": [{"key": "eventName", "value": "本編・懇親会"}], "secondaryFields": [{"key": "ticketNumber", "label": "チケット番号", "value": "S-6"}] } } 36
  11. pass.json抜粋 (基本的な設定) { "formatVersion": 1, "passTypeIdentifier": "pass.com.example.muno92", "serialNumber": "パス毎に一意なID", "teamIdentifier":

    "XXX", "organizationName": "PHPカンファレンス愛媛2026", "description": "PHPカンファレンス愛媛2026 本編・懇親会", "logoText": "PHPカンファレンス愛媛2026" } 37
  12. pass.json抜粋 (イベントチケット用の設定) { "eventTicket": { "headerFields": [ {"key": "eventName", "value":

    "本編・懇親会"} ], "secondaryFields": [ {"key": "ticketNumber","label": "チケット番号", "value": "S-6"} ] } } 39
  13. ※ "type": "event"のような設定値は無い • 「jsonにeventTicketがある = イベントチケット」と暗黙的に決定される { "eventTicket": {

    "headerFields": [ {"key": "eventName", "value": "本編・懇親会"} ], "secondaryFields": [ {"key": "ticketNumber","label": "チケット番号", "value": "S-6"} ] } } 40
  14. 「(従来の)」? • iOS 26からPoster Event Passが追加された • 若干使いづらいので詳しい説明は省略 • QRコードが使えない

    • 設定項目がスポーツイベントやライブに特化 https://developer.apple.com/documentation/walletpasses/creating-an-event-pass-using-semantic-tags 50
  15. signature • manifest.jsonをパス発行者の証明書で署名したファイル • 署名の形式:PKCS #7 分離形式 • 証明書:Apple Developerのページから作成

    • 以下を検証可能にしている • Apple Developer Programに登録済のアカウントが発行したパスである事 • pass.jsonに記載された発行者が正しい事 56
  16. パスクラス (例) { "id": "パスクラスの識別子", "issuerName": "PHPカンファレンス愛媛2026", "reviewStatus": "UNDER_REVIEW", "eventName":

    { "defaultValue": {"language": "ja-JP", "value": "本編"} }, "hexBackgroundColor": "背景色", "logo": { "sourceUri": {"uri": "ロゴ画像のURL"} }, "heroImage": { "sourceUri": {"uri": "バナー画像のURL"} } }, "classTemplateInfo": { "cardTemplateOverride": { "cardRowTemplateInfos": [ { "twoItems": { "startItem": { "firstValue": { "fields": [{"fieldPath": "object.ticketNumber"}] } }, "endItem": { "firstValue": { "fields": [{"fieldPath": "object.textModuleData['name']"}] } } } } ] } } 59
  17. パスクラス抜粋 (基本設定) { "id": "クラスID", "issuerName": "PHPカンファレンス愛媛2026", "reviewStatus": "UNDER_REVIEW", "eventName":

    { "defaultValue": { "language": "ja-JP", "value": "本編" • クラスID: パスクラス毎に一意な値 • reviewStatus: 登録後、 ACCEPTEDに自動で変更される } } } 60
  18. パスクラス抜粋 (画像・色指定) { "hexBackgroundColor": "背景色", "logo": { "sourceUri": {"uri": "ロゴ画像のURL"}

    }, "heroImage": { "sourceUri": {"uri": "バナー画像のURL"} • 画像はURL指定 • = インターネット上に公開されて いなければいけない } } 61
  19. パスクラス抜粋 (レイアウト指定) { } • テンプレートに則った形でレイアウトを指定 "classTemplateInfo": { "cardTemplateOverride": {

    "cardRowTemplateInfos": [ { "twoItems": { "startItem": { "firstValue": { "fields": [{"fieldPath": "object.ticketNumber"}] } }, "endItem": { "firstValue": { "fields": [{"fieldPath": "object.textModuleData['name']"}] } } } } ] } } 62
  20. パスクラス更に抜粋 (行のレイアウト指定) { "twoItems": { "startItem": { "firstValue": { "fields":

    [] } }, "endItem": { "firstValue": { "fields": [] } } } } • 行のレイアウトを2列に (1列〜3列まで指定可) 64
  21. パスクラス更に抜粋 (行のレイアウト指定) { "twoItems": { "startItem": { "firstValue": { "fields":

    [] } }, "endItem": { "firstValue": { "fields": [] } } } } • それぞれの列は1つ目の項目だけ使用する 65
  22. パスオブジェクト (例) { "id": "オブジェクトID", "classId": "紐づくパスクラスのID", "state": "ACTIVE", "textModuleData":

    [ { "id": "name", "header": "参加者名", • 共通の値はパスクラス側で設定してい るため、パス毎に独自の値を設定する だけ "body": "muno92" } ], "ticketNumber": "X-7", "barcode": { "type": "QR_CODE", "value": "QRコードに埋め込む文字列" } } 67
  23. Appleウォレットのパス開発に必要なもの • Apple Developer Programのアカウント • Web経由での登録: 99ドル相当の現地通貨/年 • Developerアプリ経由と大差ない価格との情報も

    • MacのDeveloperアプリ経由での登録: 12,800円/年 (2025年11月時点) • アカウントを持ってさえいればウォレットのパス開発に審査は不要 https://developer.apple.com/jp/programs/ 73
  24. pkpassの構築 (1) パスの種類を決める • Generic Pass / Event Pass /

    Boarding Passなど • パスはその種類によって基本的な機能やレイアウトが自動で決定される • 100%自由に実装できるのではなく、Appleの枠組みに沿った形での実装 75
  25. pkpassの構築 (5) signatureの生成 1. CSR (Certi cate Signing Request。証明書署名要求)を作成 2.

    Pass Type Identi erの登録・証明書の作成 3. 証明書・秘密鍵・中間証明書でmanifest.jsonに署名 fi fi 79
  26. CSRの作成 openssl req -new -newkey rsa:2048 -nodes -keyout key.pem -out

    cert.csr • 組織名などの入力を求められるが空でOK • 証明書作成時、Apple Developerのアカウント情報が自動設定される • manifest.jsonの署名時に秘密鍵が必要になるので、秘密鍵も生成しておく 81
  27. Pass Type Identi er • 複数のパスをまとめる識別子 • 組織内に複数設定できる • Pass

    Type Identi er & serialNumberでパスが一意になる • 推奨:逆ドメイン形式 • 例) pass.com.example.muno92 https://developer.apple.com/documentation/walletpasses/building-a-pass#Create-a-Pass-Type-Identi er fi fi fi 82
  28. Pass Type Identi erの登録・証明書の作成 Apple Developer向けページにて 1. Pass Type Identi

    erを登録 2. 登録したPass Type Identi erを選択し、作成したCSRをアップロード 3. 証明書が作成される https://developer.apple.com/documentation/walletpasses/building-a-pass#Create-a-Pass-Type-Identi er https://developer.apple.com/documentation/walletpasses/building-a-pass#Generate-a-Signing-Certi cate fi fi fi fi fi 83
  29. PHPで署名する実装例 openssl_cms_sign( 'manifest.jsonのパス', '署名を出力したいパス', • 署名にはopenssl̲cms̲signを使うのがオススメ openssl_x509_read(file_get_contents('証明書のパス')), • openssl̲pkcs7̲signは直接バイナリで書き出せない openssl_get_privatekey(file_get_contents('秘密鍵のパス')),

    • 自力でPEM→DERの変換を組む必要がある [], #7に互換がある署名形式なので問題なく使える) • (CMSはPKCS flags: PKCS7_DETACHED, encoding: OPENSSL_ENCODING_DER, untrusted_certificates_filename: 'Appleの中間証明書のパス' ); 90
  30. PHPで署名する実装例 openssl_cms_sign( 'manifest.jsonのパス', • WWDR (Apple Worldwide Developer Relations) 中間証明書も必要

    '署名を出力したいパス', • Appleウォレット用:G4 openssl_x509_read(file_get_contents('証明書のパス')), • https://developer.apple.com/jp/help/account/certi cates/wwdropenssl_get_privatekey(file_get_contents('秘密鍵のパス')), intermediate-certi cates/ [], flags: PKCS7_DETACHED, encoding: OPENSSL_ENCODING_DER, untrusted_certificates_filename: 'Appleの中間証明書のパス' ); fi fi 91
  31. 目次 • 前提 • パスの新規追加 • Appleウォレット • Google ウォレット

    • 追加したパスの更新 • カスタマイズするとより便利に • まとめ 95
  32. Google ウォレットのパス開発に必要なもの • Google Cloudのプロジェクト • Google Wallet API 発行者アカウント

    (無料) • 初回審査 (後述) https://developers.google.com/wallet/tickets/events/getting-started/onboarding-guide 96
  33. 実際にはSDK経由で組み立て・登録できる $eventClass = new EventTicketClass([ 'id' 'クラスID', 'issuerName' 'PHPカンファレンス愛媛2026', 'reviewStatus'

    'UNDER_REVIEW', 'eventName' new LocalizedString([ 'defaultValue' new TranslatedString([ 'language' 'ja-JP', 'value' '本編', ]), ]), ]); $this service eventticketclass insert($eventClass); > - > > = = > > = = > = > = > - > = > - 100
  34. パス保存用のJWTを生成 $jwt = JWT encode( [ 'iss' $serviceAccount['client_email'], 'aud' 'google',

    'origins' [], 'typ' 'savetowallet', 'payload' [ 'eventTicketClasses' [$passClass], 'eventTicketObjects' [$passObject], ], ], $serviceAccount['private_key'], 'RS256' ); > > = = > > = = > > > = = = : : 103
  35. 目次 • 前提 • パスの新規追加 • 追加したパスの更新 • Google ウォレット

    • Appleウォレット • カスタマイズするとより便利に • まとめ 113
  36. Google ウォレットのパス更新 • PUT or PATCH パスクラス • PUT or

    PATCH パスオブジェクト • パスオブジェクトの削除は不可能 • stateをINACTIVEに変更する 116
  37. 目次 • 前提 • パスの新規追加 • 追加したパスの更新 • Google ウォレット

    • Appleウォレット • カスタマイズするとより便利に • まとめ 117
  38. Appleウォレットのパス更新 • Appleウォレットの場合、端末上のパスは直接更新できない • どうするか • パス保存時に端末情報をサーバーに保存しておく • APNs (Apple

    Push Noti cation Service) 経由で「パスに更新があった」 とPush通知 • iPhoneからサーバーに最新のパスを取ってこさせる fi 119
  39. pkpass更新 (1) パス追加時に端末登録 pkpassファイル POST 端末情報 サーバー • passTypeIdenti er

    • パスのserialNumber • 端末ID (deviceLibraryIdenti er) • Push通知用のトークン (pushToken) fi fi 121
  40. pkpass更新 (2) 更新通知 POST 更新通知 APNs サーバー • パス発行用証明書 •

    パス発行用秘密鍵 • pushToken • ※パス情報は送信していない • 「何かのパスに更新があったよ」 122
  41. pkpass更新 (3) 最新パスの取得 GET 更新パス一覧 サーバー • deviceLibraryIdenti er •

    passTypeIdenti er • 最終更新日時 (passesUpdatedSince) fi fi 124
  42. APNsへのPOST例 (CakePHPの場合) $client = new \Cake\Http\Client([ 'protocolVersion' '2', 'curl' [

    CURLOPT_SSLCERT '証明書のパス', CURLOPT_SSLKEY '秘密鍵のパス', ], ]); $client post( "https: api.push.apple.com/3/device/{$pushToken}", '{}' ); > = > > = = > / = / > - 134
  43. APNsへのPOST例 (CakePHPの場合) $client = new \Cake\Http\Client([ 'protocolVersion' '2', 'curl' [

    CURLOPT_SSLCERT '証明書のパス', HTTP/2 必須 CURLOPT_SSLKEY '秘密鍵のパス', ], ]); $client post( "https: api.push.apple.com/3/device/{$pushToken}", '{}' ); > = > > = = > / = / > - 135
  44. APNsへのPOST例 (CakePHPの場合) $client = new \Cake\Http\Client([ 'protocolVersion' '2', 'curl' [

    CURLOPT_SSLCERT '証明書のパス', CURLOPT_SSLKEY '秘密鍵のパス', ], ]); pkpassの署名に使用した証明書・秘密鍵で認証 $client post( "https: api.push.apple.com/3/device/{$pushToken}", '{}' ); > = > > = = > / = / > - 136
  45. APNsへのPOST例 (CakePHPの場合) $client = new \Cake\Http\Client([ 'protocolVersion' '2', 'curl' [

    CURLOPT_SSLCERT '証明書のパス', CURLOPT_SSLKEY ], '秘密鍵のパス', 通知対象となる端末のトークンを指定する ]); $client post( "https: api.push.apple.com/3/device/{$pushToken}", '{}' ); > = > > = = > / = / > - 137
  46. APNsへのPOST例 (CakePHPの場合) $client = new \Cake\Http\Client([ 'protocolVersion' '2', 'curl' [

    CURLOPT_SSLCERT '証明書のパス', CURLOPT_SSLKEY '秘密鍵のパス', ], ]);payloadは空のJSONオブジェクトと定められている $client post( "https: api.push.apple.com/3/device/{$pushToken}", '{}' ); > = > > = = > / = / > - 138
  47. 通知 • 様々なトリガーをきっかけとして通知欄に表示させられる • 位置情報 • 時刻 • ビーコン (Appleウォレット限定)

    • パスの更新 https://developer.apple.com/documentation/walletpasses/showing-a-pass-on-the-lock-screen https://developers.google.com/wallet/tickets/events/use-cases/trigger-push-noti cations fi 142
  48. 色々できるからこそ、設計が重要 • どこまで情報を詰めるか • 何でもかんでも詰めると情報過多になる • どういったタイミングで通知を出すか • Appleウォレット /

    Google ウォレットで提供する機能をどこまで揃えるか • 個人的には、最低限の機能を平等に提供できていれば良いと思う • どちらかでしか使えない機能もあるので 147
  49. pkpass更新のAPI定義 • POST 端末情報 • GET 更新パス一覧 • 補足: previousLastUpdatedのフォーマットは定まっていない

    • が、個人的にはUNIXタイムスタンプを使うのがオススメ(実装が楽) • GET 更新があったパス • DELETE 端末情報 • POST ログ 158
  50. 最新情報のキャッチアップ方法 • Appleウォレット • WWDC • 例) Whatʼs new in

    Wallet (2026) • Google ウォレット • Google I/O • 例) The latest from Google Pay and Google Wallet (2026) • Wallet release notes (RSSフィード有) 159
  51. Apple Watchへの保存 • 保存動線 • Webサイトから追加する場合:iPhone経由で反映される • メールやメッセージから追加する場合:Apple Watchに直接追加される •

    パスの更新はiPhone経由 • 直接Apple Watchに保存した場合でも、一旦iPhoneにパスが自動追加さ れてiPhone経由で更新される 160
  52. 電子署名についての補足 • PKCS #7 (Public Key Cryptography Standardsの7番目) • データの暗号化や署名に使われる。RFC

    2315で定義 • 署名データの形式 • 分離(Detached)方式 • 内包(Enveloping)方式 • 実はこれでもAppleウォレットは動くが、動き続ける保証はないので Detachedで実装するのが無難 162