Slide 1

Slide 1 text

PHPerにこそ勧めたい Appleウォレット / Google ウォレット開発 2026/10/3 PHPカンファレンス愛媛2026 @muno̲92

Slide 2

Slide 2 text

自己紹介 • X (旧Twitter): @muno̲92 • 趣味:カンファレンススタッフ • PHPerKaigi • PHPカンファレンス小田原 • forteeのウォレット連携機能を作ってます 2

Slide 3

Slide 3 text

forteeのAppleウォレット・Google ウォレット機能 3

Slide 4

Slide 4 text

ウォレットって便利ですよね? • チケットを出すのが簡単 • メールから探したりWebページを開いたりしなくて済む • 特定のサービス専用のアプリを入れる必要もない • オフラインでもチケットを表示できる • 航空券だと予定変更が自動で反映されたり 4

Slide 5

Slide 5 text

開発はモバイルエンジニアの領域 と思っていませんか?

Slide 6

Slide 6 text

実は、JSONの構築 + 画像の配置 + αで出来ます

Slide 7

Slide 7 text

やれそうな気がしてきませんか?

Slide 8

Slide 8 text

ウォレットの世界に 飛び込んでみましょう

Slide 9

Slide 9 text

目次 • 前提 • データ構造 • 実装 • カスタマイズするとより便利に • まとめ 9

Slide 10

Slide 10 text

目次 • 前提 • データ構造 • 実装 • カスタマイズするとより便利に • まとめ 10

Slide 11

Slide 11 text

Appleウォレット / Google ウォレットとは • 様々な「パス」をスマートフォン・スマートウォッチに保存可能なサービス • イベントチケット、クレジットカード、ポイントカード、航空券など https://developer.apple.com/wallet/ https://developers.google.com/wallet/ 11

Slide 12

Slide 12 text

今回はforteeのパスをベースに話します • forteeのパス = イベントチケット 12

Slide 13

Slide 13 text

この発表のゴール • ウォレットの基本的な概念を理解する • ウォレットのパス開発って面白そう・意外とやれそう!と思えるようになる 13

Slide 14

Slide 14 text

話すこと 話さないこと • ウォレットの基本的な概念 • 異常系でのデバッグ方法 • パスの新規追加 / 更新の流れ • 正常系only • (スライドの末尾に付録として載 せています) • パスの種類毎の細かい実装方法 14

Slide 15

Slide 15 text

目次 • 前提 • データ構造 • 実装 • カスタマイズするとより便利に • まとめ 15

Slide 16

Slide 16 text

パスのデータ構造 • 概要 • 細かいデータ構造 • Appleウォレット • Google ウォレット 16

Slide 17

Slide 17 text

メインはJSON • AppleウォレットもGoogle ウォレットもメインとなるデータはJSON • データをどうやって端末に持たせるかに違いがある 17

Slide 18

Slide 18 text

Appleウォレットのパス = 実はzip • 拡張子を.pkpassに変更しただけのzipファイル 18

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

pkpassをダウンロードするとパスが追加される • 追加対象:端末 • 同じApple Accountで複数端末持っていても、追加されるのはpkpassを ダウンロードした端末だけ • = Appleのサーバー上にパスは存在せず、端末上にしか保存されていない 20

Slide 21

Slide 21 text

Google ウォレットのパス • Googleのサーバー上に保存された2つのJSON • パスクラス:パス共通の情報 (イベント情報、レイアウト指定など) • パスオブジェクト:パス独自の情報 (参加者情報など) 21

Slide 22

Slide 22 text

パスクラス (例) { "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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

パスクラス (イメージ) パスクラス側に設定する共通の値 logo issuerName eventName heroImage 24

Slide 25

Slide 25 text

パスクラス (イメージ) パスクラス側に設定する共通の値 logo issuerName eventName object.textModuleData['name'] object.ticketNumber パスオブジェクトを埋め込むための参照 heroImage 25

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

パスが作成される 27

Slide 28

Slide 28 text

パス保存用URLにアクセスするとパスが追加される • 追加対象:Google アカウント • 例えばPC上で追加操作を行うとスマートフォンにも追加される • = Googleのサーバー上にパスが保存されている 28

Slide 29

Slide 29 text

パスのデータ構造 • 概要 • 細かいデータ構造 • Appleウォレット • Google ウォレット 29

Slide 30

Slide 30 text

再掲) 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

Slide 31

Slide 31 text

(例) pkpassに含まれるファイル • pass.json • logo.png • icon.png • strip.png • manifest.json • signature 31

Slide 32

Slide 32 text

(例) pkpassに含まれるファイル • pass.json パス情報が記載されたメインのファイル • logo.png • icon.png • strip.png • manifest.json • signature 32

Slide 33

Slide 33 text

(例) pkpassに含まれるファイル • pass.json パス情報が記載されたメインのファイル • logo.png • icon.png 画像ファイル • strip.png • manifest.json • signature 33

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

pass.json抜粋 (基本的な設定) { "formatVersion": 1, "passTypeIdentifier": "pass.com.example.muno92", "serialNumber": "パス毎に一意なID", "teamIdentifier": "XXX", "organizationName": "PHPカンファレンス愛媛2026", "description": "PHPカンファレンス愛媛2026 本編・懇親会", "logoText": "PHPカンファレンス愛媛2026" } 37

Slide 38

Slide 38 text

pass.json抜粋 (QRコード用の設定) { } "barcode": { "message": "QRコードに埋め込む文字列", } "format": "PKBarcodeFormatQR", "messageEncoding": "iso-8859-1" 38

Slide 39

Slide 39 text

pass.json抜粋 (イベントチケット用の設定) { "eventTicket": { "headerFields": [ {"key": "eventName", "value": "本編・懇親会"} ], "secondaryFields": [ {"key": "ticketNumber","label": "チケット番号", "value": "S-6"} ] } } 39

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

画像ファイル • icon.png: 必須 • 通知などで使われる • それ以外の画像 • パスの種類やレイアウトに応じる logo.png strip.png 41

Slide 42

Slide 42 text

icon.png以外の必要な画像は パスやレイアウトによって変わる

Slide 43

Slide 43 text

レイアウトはどうやって決まる?

Slide 44

Slide 44 text

レイアウトはpass.jsonだけでは決まらない • (従来の)イベントチケットには2種類のレイアウトがある サムネイル画像有のレイアウト バナー画像有のレイアウト 44

Slide 45

Slide 45 text

画像の有無によってレイアウトが決まる • thumbnail.pngがある • → サムネイル画像有のレイアウト • strip.png (横長のバナー画像) がある • → バナー画像有のレイアウト • (両方ある場合もこっち) 45

Slide 46

Slide 46 text

レイアウトによって使用可能な領域も変わる サムネイル画像有のレイアウト バナー画像有のレイアウト 46

Slide 47

Slide 47 text

レイアウトによって使用可能な領域も変わる Primary eldsはStrip imageに重なる サムネイル画像有のレイアウト バナー画像有のレイアウト fi 47

Slide 48

Slide 48 text

Primary FieldsはStrip imageに重なる • それを前提にした画像にしていない限り、 Primary Fieldsが実質使えなくなる • 使用可能な領域が1つ減る 48

Slide 49

Slide 49 text

何の画像を使うかも意外と大事

Slide 50

Slide 50 text

「(従来の)」? • iOS 26からPoster Event Passが追加された • 若干使いづらいので詳しい説明は省略 • QRコードが使えない • 設定項目がスポーツイベントやライブに特化 https://developer.apple.com/documentation/walletpasses/creating-an-event-pass-using-semantic-tags 50

Slide 51

Slide 51 text

パスの表示などに関わるのは2種類のファイル • pass.json • 画像 51

Slide 52

Slide 52 text

パスの表示などに関わるのは2種類のファイル • pass.json • 画像 • あとの2つは? • manifest.json • signature 52

Slide 53

Slide 53 text

pkpassはただのzip • という事は、何も対策しないと改ざんや発行元の詐称が簡単にできてしまう 53

Slide 54

Slide 54 text

pkpassはただのzip • という事は、何も対策しないと改ざんや発行元の詐称が簡単にできてしまう • そこで、manifest.json、signatureを生成・格納している • 改ざん防止→manifest.json (各ファイルのハッシュ値を記載) • 発行者の検証→signature (発行者の証明書による署名) 54

Slide 55

Slide 55 text

manifest.json • 改ざん検知用に各ファイルのSHA-1ハッシュを記載 { } "logo.png": "d51acb2266839519f4e15e5bb1bc9f2ca31a081f", "icon.png": "d51acb2266839519f4e15e5bb1bc9f2ca31a081f", "strip.png": "3d3279b535a9240a47562d94521d00c175a23bf3", "pass.json": "926015b099b5e3d28fe2ae1cf60fecef95ab2acc" 55

Slide 56

Slide 56 text

signature • manifest.jsonをパス発行者の証明書で署名したファイル • 署名の形式:PKCS #7 分離形式 • 証明書:Apple Developerのページから作成 • 以下を検証可能にしている • Apple Developer Programに登録済のアカウントが発行したパスである事 • pass.jsonに記載された発行者が正しい事 56

Slide 57

Slide 57 text

パスのデータ構造 • 概要 • 細かいデータ構造 • Appleウォレット • Google ウォレット 57

Slide 58

Slide 58 text

再掲) Google ウォレットのパス • Googleのサーバー上に保存された2つのJSON • パスクラス:パス共通の情報 (イベント情報、レイアウト指定など) • パスオブジェクト:パス独自の情報 (参加者情報など) 58

Slide 59

Slide 59 text

パスクラス (例) { "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

Slide 60

Slide 60 text

パスクラス抜粋 (基本設定) { "id": "クラスID", "issuerName": "PHPカンファレンス愛媛2026", "reviewStatus": "UNDER_REVIEW", "eventName": { "defaultValue": { "language": "ja-JP", "value": "本編" • クラスID: パスクラス毎に一意な値 • reviewStatus: 登録後、 ACCEPTEDに自動で変更される } } } 60

Slide 61

Slide 61 text

パスクラス抜粋 (画像・色指定) { "hexBackgroundColor": "背景色", "logo": { "sourceUri": {"uri": "ロゴ画像のURL"} }, "heroImage": { "sourceUri": {"uri": "バナー画像のURL"} • 画像はURL指定 • = インターネット上に公開されて いなければいけない } } 61

Slide 62

Slide 62 text

パスクラス抜粋 (レイアウト指定) { } • テンプレートに則った形でレイアウトを指定 "classTemplateInfo": { "cardTemplateOverride": { "cardRowTemplateInfos": [ { "twoItems": { "startItem": { "firstValue": { "fields": [{"fieldPath": "object.ticketNumber"}] } }, "endItem": { "firstValue": { "fields": [{"fieldPath": "object.textModuleData['name']"}] } } } } ] } } 62

Slide 63

Slide 63 text

パスクラス更に抜粋 (書き換える行の指定) { "classTemplateInfo": { "cardTemplateOverride": { "cardRowTemplateInfos": [ {"twoItems": { ] } } } • 自由に使えるCard Templateの 1つ目を上書き } } 63

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

パスクラス更に抜粋 (行のレイアウト指定) { "twoItems": { "startItem": { "firstValue": { "fields": [] } }, "endItem": { "firstValue": { "fields": [] } } } } • それぞれの列は1つ目の項目だけ使用する 65

Slide 66

Slide 66 text

パスクラス更に抜粋 (パスオブジェクトの参照) "fields": [{"fieldPath": "object.ticketNumber"}] "fields": [{"fieldPath": "object.textModuleData['name']"}] • パスクラスに値を直接埋め込むのではなくパスオジェクトを参照している • 参照方法 • 既定のプロパティ→プロパティ名 • 独自定義の値→textModuleData['id名'] or textModuleData[$i] 66

Slide 67

Slide 67 text

パスオブジェクト (例) { "id": "オブジェクトID", "classId": "紐づくパスクラスのID", "state": "ACTIVE", "textModuleData": [ { "id": "name", "header": "参加者名", • 共通の値はパスクラス側で設定してい るため、パス毎に独自の値を設定する だけ "body": "muno92" } ], "ticketNumber": "X-7", "barcode": { "type": "QR_CODE", "value": "QRコードに埋め込む文字列" } } 67

Slide 68

Slide 68 text

ここまでがパスの基本的な構造

Slide 69

Slide 69 text

パスを実装してみましょう

Slide 70

Slide 70 text

目次 • 前提 • データ構造 • 実装 • カスタマイズするとより便利に • まとめ 70

Slide 71

Slide 71 text

実装 • パスの新規追加 • Appleウォレット • Google ウォレット • 追加したパスの更新 71

Slide 72

Slide 72 text

Appleウォレットのパス開発に必要なもの • Apple Developer Programのアカウント • Web経由での登録: 99ドル相当の現地通貨/年 • Developerアプリ経由と大差ない価格との情報も • MacのDeveloperアプリ経由での登録: 12,800円/年 (2025年11月時点) https://developer.apple.com/jp/programs/ 72

Slide 73

Slide 73 text

Appleウォレットのパス開発に必要なもの • Apple Developer Programのアカウント • Web経由での登録: 99ドル相当の現地通貨/年 • Developerアプリ経由と大差ない価格との情報も • MacのDeveloperアプリ経由での登録: 12,800円/年 (2025年11月時点) • アカウントを持ってさえいればウォレットのパス開発に審査は不要 https://developer.apple.com/jp/programs/ 73

Slide 74

Slide 74 text

pkpass構築の流れ 1. パスの種類を決める 2. pass.jsonを組む 3. 画像を配置する 4. manifest.jsonを生成する 5. signatureを生成する 6. zipで固めて拡張子を変更する 74

Slide 75

Slide 75 text

pkpassの構築 (1) パスの種類を決める • Generic Pass / Event Pass / Boarding Passなど • パスはその種類によって基本的な機能やレイアウトが自動で決定される • 100%自由に実装できるのではなく、Appleの枠組みに沿った形での実装 75

Slide 76

Slide 76 text

pkpass構築 (2) pass.jsonを組む • ドキュメントの型定義を参考にjsonを組んでいく • masOS 27以降であればPass Designer (beta)が便利 • GUIでレイアウトを組める 76

Slide 77

Slide 77 text

pkpass構築 (3) 画像を配置する • 画像の一覧・サイズの要件などがまとまったドキュメントを参考に必要な 画像を用意する • ※2026/10/3現在、Strip imageがEvent Ticketで使えないような記述に なっていますが実際は使えます 77

Slide 78

Slide 78 text

pkpassの構築 (4) manifest.jsonの生成 • 改ざん検知用に各ファイルのSHA-1ハッシュを計算する json_encode([ 'logo.png' sha1_file('logo.pngのパス'), 'icon.png' sha1_file('icon.pngのパス'), 'strip.png' sha1_file('strip.pngのパス'), 'pass.json' sha1('pass.jsonの文字列'), ]); > > > = > = = = 78

Slide 79

Slide 79 text

pkpassの構築 (5) signatureの生成 1. 署名のための証明書を作成する 1. CSR (Certi cate Signing Request。証明書署名要求)を作成 2. Pass Type Identi erの登録・証明書の作成 2. 証明書・秘密鍵・中間証明書でmanifest.jsonに署名 fi fi 79

Slide 80

Slide 80 text

CSR • Certi cate Signing Request。証明書署名要求 • 証明書の元となるファイル。発行者情報や公開鍵などが含まれる • これに対してAppleが署名して証明書が作成される fi 80

Slide 81

Slide 81 text

CSRの作成 openssl req -new -newkey rsa:2048 -nodes -keyout key.pem -out cert.csr • 組織名などの入力を求められるが空でOK • 証明書作成時、Apple Developerのアカウント情報が自動設定される • manifest.jsonの署名時に秘密鍵が必要になるので、秘密鍵も生成しておく 81

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

証明書補足(1) PHPの場合はPEM形式に変換 • 生成される証明書はDER(バイナリ)形式 • PHPのopenssl̲x509̲read関数はDER形式に対応していない • PEM (Base64エンコードしてラップしたテキスト) 形式に変換する • openssl x509 -inform der -in pass.cer -out cert.pem 84

Slide 85

Slide 85 text

証明書補足(2) 有効期限は1年 • 有効期限が切れた場合 • 署名済みのパスは使い続けられる • 新規の署名やパスの更新は不可 https://developer.apple.com/wallet/get-started/ 85

Slide 86

Slide 86 text

証明書補足(2) 有効期限は1年 • 有効期限が切れた場合 • 署名済みのパスは使い続けられる • 新規の署名やパスの更新は不可 • 毎年更新が必要 • CSRの生成・アップロードを再度行う https://developer.apple.com/wallet/get-started/ 86

Slide 87

Slide 87 text

これで署名用の証明書を 作成できた

Slide 88

Slide 88 text

次は実際に署名ファイルを作成

Slide 89

Slide 89 text

再掲) 署名ファイルの形式 • manifest.jsonをパス発行者の証明書で署名したファイル • 署名の形式:PKCS #7 分離形式 https://developer.apple.com/documentation/walletpasses/building-a-pass#Sign-the-Pass-and-Create-the-Bundle 89

Slide 90

Slide 90 text

PHPで署名する実装例 openssl_cms_sign( 'manifest.jsonのパス', '署名を出力したいパス', openssl_x509_read(file_get_contents('証明書のパス')), openssl_get_privatekey(file_get_contents('秘密鍵のパス')), [], flags: PKCS7_DETACHED, encoding: OPENSSL_ENCODING_DER, untrusted_certificates_filename: 'Appleの中間証明書のパス' ); 90

Slide 91

Slide 91 text

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の中間証明書のパス' ); 91

Slide 92

Slide 92 text

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 92

Slide 93

Slide 93 text

zip圧縮し、拡張子を変更すればpkpassになる 93

Slide 94

Slide 94 text

あとはパスをダウンロードさせる動線 • 「Appleウォレットに追加」ボタンをガイドラインに沿って設置 • このページから画像をダウンロード可能 • 🚫 ボタンの自作や改変などは禁止 94

Slide 95

Slide 95 text

これで完成 🎉 95

Slide 96

Slide 96 text

目次 • 前提 • パスの新規追加 • Appleウォレット • Google ウォレット • 追加したパスの更新 • カスタマイズするとより便利に • まとめ 96

Slide 97

Slide 97 text

Google ウォレットのパス開発に必要なもの • Google Cloudのプロジェクト • Google Wallet API 発行者アカウント (無料) • 初回審査 (後述) https://developers.google.com/wallet/tickets/events/getting-started/onboarding-guide 97

Slide 98

Slide 98 text

Google ウォレット パス新規作成の実装 1. パスの種類を決める 2. パスクラス・パスオブジェクトの登録 3. パス保存用のJWTを付与したURLを生成 4. 「Google ウォレットに追加」ボタンの設置 5. 初回審査 98

Slide 99

Slide 99 text

パスの種類の決定 • パスの種類によって基本的な機能やレイアウトが決まってくるのはAppleウォ レットと同じ 99

Slide 100

Slide 100 text

Google ウォレット パス新規作成の実装 1. パスの種類を決める 2. パスクラス・パスオブジェクトの登録 3. パス保存用のJWTを付与したURLを生成 4. 「Google ウォレットに追加」ボタンの設置 5. 初回審査 100

Slide 101

Slide 101 text

パスクラス・パスオブジェクトの登録 1. ドキュメントの型定義を参考にJSONを組み立て • Web上でプレビューできるPass Builderが便利 2. 登録用APIにPOST 101

Slide 102

Slide 102 text

実際には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); > - > > = = > > = = > = > = > - > = > - 102

Slide 103

Slide 103 text

注意点 • パスの種類は用途にあったものを使わなければいけない • ⚠ 汎用パスは「イベントパス等の種類がどれも当てはまらなかった」場合用 • 「なんにでも使える種類」では無い • 過去、審査で指摘された 103

Slide 104

Slide 104 text

Google ウォレット パス新規作成の実装 1. パスの種類を決める 2. パスクラス・パスオブジェクトの登録 3. パス保存用のJWTを付与したURLを生成 4. 「Google ウォレットに追加」ボタンの設置 5. 初回審査 104

Slide 105

Slide 105 text

パス保存用のJWTを生成 $jwt = JWT encode( [ 'iss' $serviceAccount['client_email'], 'aud' 'google', 'origins' [], 'typ' 'savetowallet', 'payload' [ 'eventTicketClasses' [$passClass], 'eventTicketObjects' [$passObject], ], ], $serviceAccount['private_key'], 'RS256' ); > > = = > > = = > > > = = = : : 105

Slide 106

Slide 106 text

パス保存用のURLを組み立て "https: pay.google.com/gp/v/save/{$jwt}" • このURLにアクセスするとパスが追加される / / 106

Slide 107

Slide 107 text

Google ウォレット パス新規作成の実装 1. パスの種類を決める 2. パスクラス・パスオブジェクトの登録 3. パス保存用のJWTを付与したURLを生成 4. 「Google ウォレットに追加」ボタンの設置 5. 初回審査 107

Slide 108

Slide 108 text

「Google ウォレットに追加」ボタン • Appleウォレットと同様、ブランドガイドラインが ある • このページから画像をダウンロード可能 • 🚫 ボタンの自作などは禁止 108

Slide 109

Slide 109 text

「Google ウォレットに追加」ボタン • Appleウォレットと同様、ブランドガイドラインが ある • このページから画像をダウンロード可能 • 🚫 ボタンの自作などは禁止 • ⚠ ローカライズされたボタンの使用が必須 • 英語ボタンはNG (過去に審査で指摘された) 109

Slide 110

Slide 110 text

Google ウォレット パス新規作成の実装 1. パスの種類を決める 2. パスクラス・パスオブジェクトの登録 3. パス保存用のJWTを付与したURLを生成 4. 「Google ウォレットに追加」ボタンの設置 5. 初回審査 110

Slide 111

Slide 111 text

初回審査 • 実装完了後、パスを公開するには審査を通さないといけない • (1回審査を通ればその後の改修では審査不要) https://developers.google.com/wallet/tickets/events/test-and-go-live/request-publishing-access 111

Slide 112

Slide 112 text

審査を通ればパスを配布できるように 🎉 112

Slide 113

Slide 113 text

最低限パスを追加 できるようになった

Slide 114

Slide 114 text

ちゃんと運用するならパスの更新も必要 • チケットがキャンセルされるかもしれない • イベント日時が変わるかもしれない • 通知(後述)のトリガーを変えたくなるかもしれない • など 114

Slide 115

Slide 115 text

目次 • 前提 • パスの新規追加 • 追加したパスの更新 • Google ウォレット • Appleウォレット • カスタマイズするとより便利に • まとめ 115

Slide 116

Slide 116 text

Google ウォレットの パスの更新はとても簡単

Slide 117

Slide 117 text

Google ウォレットのパス更新 • PUT or PATCH パスクラス • PUT or PATCH パスオブジェクト 117

Slide 118

Slide 118 text

Google ウォレットのパス更新 • PUT or PATCH パスクラス • PUT or PATCH パスオブジェクト • パスオブジェクトの削除は不可能 • stateをINACTIVEに変更する 118

Slide 119

Slide 119 text

目次 • 前提 • パスの新規追加 • 追加したパスの更新 • Google ウォレット • Appleウォレット • カスタマイズするとより便利に • まとめ 119

Slide 120

Slide 120 text

Appleウォレットのパス更新 • Appleウォレットの場合、端末上のパスは直接更新できない 120

Slide 121

Slide 121 text

Appleウォレットのパス更新 • Appleウォレットの場合、端末上のパスは直接更新できない • どうするか • パス保存時に端末情報をサーバーに保存しておく • APNs (Apple Push Noti cation Service) 経由で「パスに更新があった」 とPush通知 • iPhoneからサーバーに最新のパスを取ってこさせる fi 121

Slide 122

Slide 122 text

pkpass更新 (1) パス追加時に端末登録 pkpassファイル サーバー 122

Slide 123

Slide 123 text

pkpass更新 (1) パス追加時に端末登録 pkpassファイル POST 端末情報 サーバー • passTypeIdenti er • パスのserialNumber • 端末ID (deviceLibraryIdenti er) • Push通知用のトークン (pushToken) fi fi 123

Slide 124

Slide 124 text

pkpass更新 (2) 更新通知 POST 更新通知 APNs サーバー • パス発行用証明書 • パス発行用秘密鍵 • pushToken • ※パス情報は送信していない • 「何かのパスに更新があったよ」 124

Slide 125

Slide 125 text

pkpass更新 (2) 更新通知 更新通知 POST 更新通知 APNs サーバー 「あのサーバーの何かのパスに更新があったよ」 (画面上には表示されないサイレントPush) 125

Slide 126

Slide 126 text

pkpass更新 (3) 最新パスの取得 GET 更新パス一覧 サーバー • deviceLibraryIdenti er • passTypeIdenti er • 最終更新日時 (passesUpdatedSince) fi fi 126

Slide 127

Slide 127 text

pkpass更新 (3) 最新パスの取得 GET 更新パス一覧 更新パス一覧 サーバー • serialNumbers • lastUpdated 127

Slide 128

Slide 128 text

pkpass更新 (3) 最新パスの取得 GET 更新パス一覧 更新パス一覧 GET 更新があったパス サーバー • passTypeIdenti er • serialNumber fi 128

Slide 129

Slide 129 text

pkpass更新 (3) 最新パスの取得 GET 更新パス一覧 更新パス一覧 GET 更新があったパス サーバー pkpassファイル Last-Modi edヘッダ fi 129

Slide 130

Slide 130 text

pkpass更新 (4) 端末情報削除 • 端末からパスを削除した場合、更新は不要になるので端末情報を削除する DELETE 端末情報 サーバー • deviceLibraryIdenti er • passTypeIdenti er • serialNumber fi fi 130

Slide 131

Slide 131 text

パス更新系にはもう1つAPIが • ログ受信用API • MUSTではないが、実装しておくとデバッグが楽 POST ログ サーバー 文字列の配列 (logs) 131

Slide 132

Slide 132 text

iPhoneはどうやって サーバーにアクセスする?

Slide 133

Slide 133 text

鍵はpass.json { "passTypeIdentifier": "pass.com.example.muno92", "serialNumber": "パス毎に一意なID", "eventTicket": {}, "webServiceURL": "パス更新系APIのbase URL", "authenticationToken": "APIアクセス用の認証トークン" } 133

Slide 134

Slide 134 text

鍵はpass.json • 「POST {webServiceURL}/端末情報」のようにリクエスト • Authorizationヘッダ → ApplePass: {authenticationToken} • (詳しいAPI定義はスライド末尾の付録参照) 134

Slide 135

Slide 135 text

補足: APNsへのPOST

Slide 136

Slide 136 text

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}", '{}' ); > = > > = = > / = / > - 136

Slide 137

Slide 137 text

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}", '{}' ); > = > > = = > / = / > - 137

Slide 138

Slide 138 text

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}", '{}' ); > = > > = = > / = / > - 138

Slide 139

Slide 139 text

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}", '{}' ); > = > > = = > / = / > - 139

Slide 140

Slide 140 text

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}", '{}' ); > = > > = = > / = / > - 140

Slide 141

Slide 141 text

これでパスの追加・更新と 基本的な実装は完了

Slide 142

Slide 142 text

目次 • 前提 • パスの新規追加 • 追加したパスの更新 • カスタマイズするとより便利に • まとめ 142

Slide 143

Slide 143 text

カスタマイズするとより便利に • 通知 • 関連情報の記載 • リッチなデザインにする (Appleウォレット) 143

Slide 144

Slide 144 text

通知 • 様々なトリガーをきっかけとして通知欄に表示させられる • 位置情報 • 時刻 • ビーコン (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 144

Slide 145

Slide 145 text

関連情報の記載 • マップなどを紐づけ可能 Appleウォレット (iOS 27〜) Google ウォレット 145

Slide 146

Slide 146 text

リッチなデザインにする • これまではパスの一部しかデザインを入れる余地が無かった 146

Slide 147

Slide 147 text

リッチなデザインにする • AppleウォレットではiOS 27から背景全体に画像を設定可能に https://developer.apple.com/documentation/walletpasses/creating-a-poster-generic-pass 147

Slide 148

Slide 148 text

ウォレットを使うと色々できる • サーバーから端末に手を伸ばせる • しかもApple・Googleが定義したデータ形式に従うだけ • 面白い 148

Slide 149

Slide 149 text

色々できるからこそ、設計が重要 • どこまで情報を詰めるか • 何でもかんでも詰めると情報過多になる • どういったタイミングで通知を出すか • Appleウォレット / Google ウォレットで提供する機能をどこまで揃えるか • 個人的には、最低限の機能を平等に提供できていれば良いと思います • どちらかでしか使えない機能もあるので 149

Slide 150

Slide 150 text

ガイドラインが設計の参考になる • Apple • Human Interface Guidelines • Google • Pass Creation Guidelines 150

Slide 151

Slide 151 text

目次 • 前提 • パスの新規追加 • 追加したパスの更新 • カスタマイズするとより便利に • まとめ 151

Slide 152

Slide 152 text

まとめ • AppleウォレットもGoogle ウォレットもメインのデータはJSON • 仕様に沿ってデータを構築するだけで便利な機能が使えるようになる • 似ている所も多いが、細かい所で違いがある • データ構造、パスが紐づく対象(アカウント/端末)、費用や審査の有無など • Appleウォレットの更新にはサーバーの実装が必須 • なのでむしろサーバーサイドエンジニア向きの技術だと思います 152

Slide 153

Slide 153 text

是非トライしてみて下さい

Slide 154

Slide 154 text

最後に宣伝: PHPカンファレンス小田原2027 • 開催日:2027/4/10 (土) • 会場:おだわら市民交流センター「UMECO」 • 2026/10/13 (火)から個人スポンサー募集開始予定!! https://phpcon-odawara.jp/2027/ 154

Slide 155

Slide 155 text

ご清聴ありがとうございました

Slide 156

Slide 156 text

付録

Slide 157

Slide 157 text

動作確認方法 • Appleウォレット / Google ウォレットどちらもある程度はシミュレーターや PC・Web上のビューアーで確認できる • 通知など確認しようとすると実機が必要 157

Slide 158

Slide 158 text

デバッグ方法 (Appleウォレット) • 実装をミスっていても画面上には何もエラーが出ない • MacのConsole.appでログを確認する • ログが出力されるプロセス • Mac上の簡易ビューアー:Pass Viewer • iPhone:PassBook 158

Slide 159

Slide 159 text

デバッグ方法 (Google ウォレット) • 何かしら間違っていた場合はAPIからエラーが返ってくる 159

Slide 160

Slide 160 text

pkpass更新のAPI定義 • POST 端末情報 • GET 更新パス一覧 • 補足: previousLastUpdatedのフォーマットは定まっていない • が、個人的にはUNIXタイムスタンプを使うのがオススメ(実装が楽) • GET 更新があったパス • DELETE 端末情報 • POST ログ 160

Slide 161

Slide 161 text

最新情報のキャッチアップ方法 • 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フィード有) 161

Slide 162

Slide 162 text

Apple Watchへの保存 • 保存動線 • Webサイトから追加する場合:iPhone経由で反映される • メールやメッセージから追加する場合:Apple Watchに直接追加される • パスの更新はiPhone経由 • 直接Apple Watchに保存した場合でも、一旦iPhoneにパスが自動追加さ れてiPhone経由で更新される 162

Slide 163

Slide 163 text

スマートウォッチへの保存 (Google ウォレット) • スマートフォンに保存したパスが自動同期される https://support.google.com/googlepixelwatch/answer/14260807 163

Slide 164

Slide 164 text

電子署名についての補足 • PKCS #7 (Public Key Cryptography Standardsの7番目) • データの暗号化や署名に使われる。RFC 2315で定義 • 署名データの形式 • 分離(Detached)方式 • 内包(Enveloping)方式 • 実はこれでもAppleウォレットは動くが、動き続ける保証はないので Detachedで実装するのが無難 164

Slide 165

Slide 165 text

pkpassの覗き見 • pkpassは実質zip • 「このパスはこんな実装をしてるのか」を簡単に見れる • iPhoneでしか保存動線が表示されない無い場合もある • その時は共有でMacに送れば保存可能 • 共有OFFにしてるパスもある • Proxymanなどのプロキシツールを使えばpkpassをキャプチャできる 165

Slide 166

Slide 166 text

FAQ (Appleウォレット) • manifest.json記載の値とハッシュが一致しなかったら? • エラー • 署名をPEM形式にしたら? • パスの読み取りに失敗する • 画像にpng以外のファイルは使える? • No。pngだけ 166