Apple Wallet用のパスを作るのはやたらと難しい

## はじめに

Apple Walletのパス（チケットやポイントカード等）を作成するためのシステムを構築していると、思わぬところで「なぜこんなに複雑なのか」と頭を抱えることがあります。特に証明書周りのトラブルは、一見単純なのに解決に何時間もかかることも。

この記事では、Firebase Cloud Functionsを使ってApple Wallet用パス（.pkpass）を生成する際に直面した問題と、その原因究明・解決までの道のりを共有します。

## 実現したかったこと

- Firebase上で構築したWebアプリから、Apple WalletとGoogle Walletの両方のパスを発行
- フロントエンドでパスのデザインを設定し、サーバー側でパスを動的に生成
- ユーザーにパスのダウンロードURLを提供

## 環境

- Firebase Cloud Functions (Node.js 18)
- [passkit-generator](https://github.com/walletpass/pass-js) ライブラリ
- macOS (Sonoma/Ventura)
- Apple Developer Program アカウント

## 発行までの流れ（理想）

1. Pass Type ID証明書をApple Developer Programで取得
2. .pkpass形式のパスをNode.jsで生成
3. Cloud Storageにアップロード
4. ユーザーに署名付きURLを提供してダウンロード

## 実際に出くわした「Invalid PEM formatted message」エラー

しかし、実際にはこんなエラーに悩まされました：

```
Error generating Apple Pass: Error: Invalid PEM formatted message.
```

このエラーの原因究明と解決までの過程を紹介します。

## Pass Type IDの証明書取得手順

まず前提として、Apple Wallet用パスを発行するには、証明書の取得から始める必要があります：

1. [Apple Developer Program](https://developer.apple.com/)にログイン
2. 「Certificates, Identifiers & Profiles」セクションに移動
3. 「Identifiers」→「Pass Type IDs」を選択
4. 「+」ボタンからPass Type IDを新規登録（例: `pass.com.mycompany.myapp`）
5. 「Certificates」→「+」で新規証明書作成
6. 「Pass Type ID Certificate」を選択
7. KeychainからCSR（証明書署名リクエスト）を作成・アップロード
8. 発行された証明書をダウンロードしてKeychain Accessにインストール

ここまでは比較的直感的ですが、実際のパス生成時に必要なものは：

- Pass Type ID証明書（.p12形式に変換）
- Apple WWDR中間証明書（.pem形式）
- Pass Type ID証明書のパスワード

## 証明書周りの複雑さ

### 1. .p12ファイルのエクスポート

Keychain AccessからPass Type ID証明書をエクスポートする際、次のような問題が潜んでいます：

- 証明書だけでなく**秘密鍵も一緒に**選択してエクスポートする必要がある
- 暗号化方式がデフォルトでRC2（40bit）という古い形式になる
- この暗号化形式はNode.js環境やOpenSSL 3系では扱えない

### 2. WWDR中間証明書

Apple Worldwide Developer Relations Certificate（WWDR証明書）も必要で：

- [Appleのサイト](https://www.apple.com/certificateauthority/)からダウンロード
- G4証明書（最新）を使用する
- 空白行や余計な文字が混じると「Invalid PEM」エラーの原因になる

### 3. 証明書の形式と暗号化

最初に出会ったエラー「Invalid PEM formatted message」の原因として、次のようなことがありました：

- RC2暗号化されたp12ファイルがNode.jsで処理できない
- PEMファイル内の余計な空白行や改行が問題
- 証明書や秘密鍵が正しくエクスポートされていない

## エラー解決への道のり

### 問題1: p12ファイルの暗号化形式

キーチェーンアクセスからエクスポートしたp12ファイルを調べるとRC2暗号化でした：

```
$ openssl pkcs12 -in PassMint.p12 -info -nodes
Enter Import Password:
MAC: sha1, Iteration 1
MAC length: 20, salt length: 8
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Error outputting keys and certificates
800CAE0E02000000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:355:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()
```

エラーの内容：`Algorithm (RC2-40-CBC : 0)`はサポート外とのこと。

### 解決策1: Triple DESでp12を作り直す

OpenSSLを使って証明書を一度PEM形式で抽出し、Triple DES暗号化でp12を再作成：

```bash
# 1. PEM形式で証明書と秘密鍵を抽出（OpenSSL 1.1系が必要）
openssl pkcs12 -in original.p12 -out cert.pem -clcerts -nokeys
openssl pkcs12 -in original.p12 -out key.pem -nocerts -nodes

# 2. Triple DES暗号化でp12を再作成
openssl pkcs12 -export -in cert.pem -inkey key.pem -out signer_3des.p12 -certfile wwdr.pem -descert
```

### 問題2: wwdr.pemの内容

wwdr.pem内の空白行や余計な改行も「Invalid PEM」エラーの原因になります。

正しいPEM形式は：
```
-----BEGIN CERTIFICATE-----
(Base64エンコードされた証明書データ)
-----END CERTIFICATE-----
```

余計な空白行や改行があると、Node.js環境でパースエラーになります。

### 問題3: Base64エンコード

p12ファイルをSecret Managerに保存するためにBase64エンコードする際も注意点があります：

```bash
# 改行を含まないBase64文字列にする必要がある
base64 < signer_3des.p12 | tr -d '\n' > signer_3des.p12.b64

# またはOpenSSLで
openssl base64 -in signer_3des.p12 -A -out signer_3des.p12.b64
```

## 実装上の注意点

passkit-generatorライブラリを使う際の実装例：

```typescript
// Secret Managerから証明書とパスワードを取得
const appleP12Base64 = await getSecret('APPLE_P12');
const p12Pass = await getSecret('P12_PASS');
const p12 = Buffer.from(appleP12Base64, "base64");

// 一時ファイルに保存（Cloud Functionsでは必要なケースが多い）
const p12TempPath = path.join(tempDir, `signerKey-${passId}.p12`);
fs.writeFileSync(p12TempPath, p12);

// PKPassオブジェクト生成
const pass = await PKPass.from({
  model: templatePath, 
  certificates: {
    wwdr: wwdrPath, // PEMファイルのパス
    signerCert: p12TempPath, // P12ファイルのパス
    signerKey: p12TempPath, // 同じP12ファイルのパス
    signerKeyPassphrase: p12Pass // P12のパスワード
  }
});

// パスをバッファとして取得
const passBuffer = await pass.getAsBuffer();
```

## 未解決の問題・疑問点

残念ながら、上記の対応をしてもNode.js環境によっては「Invalid PEM」エラーが続くことがあります。考えられる追加の原因：

1. Node.jsのバージョンによる互換性問題（18 vs 16）
2. passkit-generatorライブラリの内部実装の問題
3. OpenSSLのバージョンの違いによる暗号化/復号の非互換性
4. 証明書や秘密鍵がKeychain Accessからうまくエクスポートされていない
5. Cloud Functionsの一時ファイル操作関連の権限問題

## ベストプラクティス

現時点でのベストプラクティスをまとめると：

1. **Triple DESまたはAESでp12をエクスポート**
   - OpenSSLを使ってRC2からTriple DESに変換
   - `-descert`または`-keypbe AES-256-CBC -certpbe AES-256-CBC`オプションを使用

2. **wwdr.pemの正確な管理**
   - 余分な空白行や改行を削除
   - 正確なPEM形式を維持

3. **Base64エンコードの注意点**
   - 改行なしの1行の文字列にする
   - Secret Manager登録時にそのまま貼り付け

4. **パスワード管理**
   - 特殊文字や空白を含まないシンプルなパスワードが安全

## さいごに

Apple Wallet用パスの生成は、一見単純そうに見えて非常に複雑な要素が絡み合っています。証明書関連のエラーは根本原因の特定が難しく、解決に時間がかかることがあります。Appleのドキュメントには詳しい説明が少ないこの分野ですが、粘り強く取り組めばいつかApple Walletパスを提供できるでしょう。

## 参考資料

- [passkit-generator GitHub](https://github.com/walletpass/pass-js)
- [Apple Developer - Wallet](https://developer.apple.com/wallet/)
- [Apple Certificate Authority](https://www.apple.com/certificateauthority/)
- [OpenSSL コマンドリファレンス](https://www.openssl.org/docs/man1.1.1/man1/)