「Php/アプリストア連携/返金API/AppStore」の版間の差分
提供: 初心者エンジニアの簡易メモ
(→appstore内の返金リクエストurl) |
(→openssl_x509_store_add_certが使えない場合はコマンドで実行する) |
||
| 行51: | 行51: | ||
jsonはjwtエンコードされてるので、jwtデコードする必要がある。 | jsonはjwtエンコードされてるので、jwtデコードする必要がある。 | ||
その際に、apple証明書で、検証する。アプリ別(bundle_id別)ではなく、どのアプリでも同じ証明書で検証する。 | その際に、apple証明書で、検証する。アプリ別(bundle_id別)ではなく、どのアプリでも同じ証明書で検証する。 | ||
| − | === | + | ===opensslでapple署名検証しながらfirebaseのjwtで、jwtデコードする=== |
| + | composerで、firebase/php-jwtのインストール | ||
| + | composer require firebase/php-jwt | ||
| + | |||
| + | apple署名検証付きデコード処理 | ||
<pre> | <pre> | ||
| − | function | + | use \Firebase\JWT\JWT; |
| − | + | use \Firebase\JWT\Key; | |
| − | + | ||
| + | function verifyAppleJWSWithFirebaseJWT($signedPayload) { | ||
| + | // 1. JWSのヘッダーを手動で解析(アルゴリズム確認用) | ||
| + | [$headerBase64] = explode('.', $signedPayload); | ||
| + | $header = json_decode(base64_decode($headerBase64), true); | ||
| + | |||
| + | if ($header['alg'] !== 'ES256') { | ||
| + | throw new Exception('Invalid algorithm. Expected ES256'); | ||
} | } | ||
| − | // | + | |
| − | $ | + | // 2. Appleの証明書チェーンを準備 |
| − | + | $leafCert = $header['x5c'][0] ?? null; | |
| − | $ | + | if (!$leafCert) { |
| − | + | throw new Exception('Missing x5c certificate chain'); | |
| − | unlink($ | + | } |
| − | return | + | |
| + | // 3. 中間証明書とルート証明書をダウンロード | ||
| + | $intermediateCert = file_get_contents('https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer'); | ||
| + | $rootCert = file_get_contents('https://www.apple.com/certificateauthority/AppleRootCA-G3.cer'); | ||
| + | |||
| + | // 4. 証明書チェーンを検証 | ||
| + | $certificateChain = [ | ||
| + | 'leaf' => "-----BEGIN CERTIFICATE-----\n{$leafCert}\n-----END CERTIFICATE-----", | ||
| + | 'intermediate' => $intermediateCert, | ||
| + | 'root' => $rootCert | ||
| + | ]; | ||
| + | |||
| + | if (!verifyCertificateChain($certificateChain)) { | ||
| + | throw new Exception('Certificate chain validation failed'); | ||
| + | } | ||
| + | |||
| + | // 5. firebase/php-jwt でデコード | ||
| + | $decoded = JWT::decode( | ||
| + | $signedPayload, | ||
| + | new Key($certificateChain['leaf'], 'ES256') | ||
| + | ); | ||
| + | |||
| + | return $decoded; | ||
| + | } | ||
| + | |||
| + | // 証明書チェーン検証(簡略版) | ||
| + | function verifyCertificateChain($chain) { | ||
| + | // 1. 一時ファイルを安全に作成 | ||
| + | $tempDir = sys_get_temp_dir(); | ||
| + | $tempFile = tempnam($tempDir, 'cert_'); | ||
| + | |||
| + | // 2. 証明書をファイルに書き込み(PEM形式を保証) | ||
| + | file_put_contents($tempFile, | ||
| + | "-----BEGIN CERTIFICATE-----\n" . | ||
| + | chunk_split(base64_encode($chain['leaf']), 64) . | ||
| + | "-----END CERTIFICATE-----\n" | ||
| + | ); | ||
| + | |||
| + | // 3. ファイル権限を設定(重要) | ||
| + | chmod($tempFile, 0644); | ||
| + | |||
| + | // 4. 証明書チェーンを構築 | ||
| + | $store = openssl_x509_read("file://{$tempFile}"); | ||
| + | if ($store === false) { | ||
| + | unlink($tempFile); | ||
| + | throw new Exception('Failed to read certificate'); | ||
| + | } | ||
| + | |||
| + | // 5. 目的チェックを実行 | ||
| + | $result = openssl_x509_checkpurpose( | ||
| + | "file://{$tempFile}", | ||
| + | X509_PURPOSE_ANY, | ||
| + | [], // 追加のCA証明書は不要(Appleの証明書はシステムに組み込み) | ||
| + | $store | ||
| + | ); | ||
| + | |||
| + | // 6. 後処理 | ||
| + | unlink($tempFile); | ||
| + | openssl_x509_free($store); | ||
| + | |||
| + | return $result; | ||
} | } | ||
</pre> | </pre> | ||
2025年6月6日 (金) 18:55時点における版
目次
appleのstorekitの返金のドキュメント
https://developer.apple.com/jp/documentation/storekit/in-app_purchase/handling_refund_notifications/
ストアからの通知url設定箇所
appstore管理画面/配信/アプリ情報/appstoreサーバ通知
返金通知のサーバ側モックJSON
- subtype: "DISPUTE", or "OTHER"
- signedRenewalInfo: 省略可能(テスト時)
- signedTransactionInfo: 省略可能(テスト時)
- cancellation_reason: 1:ユーザー申請, 0:その他
{
"notificationType": "REFUND",
"subtype": "DISPUTE", // または "OTHER"
"notificationUUID": "a1b2c3d4-5678-90ef-1234-567890abcdef",
"data": {
"appAppleId": 123456789,
"bundleId": "com.example.app1",
"bundleVersion": "1.0",
"environment": "Sandbox",
"signedRenewalInfo": "...",
"signedTransactionInfo": "...",
"unifiedReceipt": {
"environment": "Sandbox",
"latest_receipt": "BASE64_ENCODED_RECEIPT_DATA",
"latest_receipt_info": [
{
"cancellation_date_ms": "1625097600000",
"cancellation_reason": "1",
"product_id": "premium_subscription",
"transaction_id": "1000000123456789",
"original_transaction_id": "1000000123456789",
"purchase_date_ms": "1625000000000",
"expires_date_ms": "1627600000000"
}
],
"status": 0
}
},
"version": "2.0"
}
appstore内の返金リクエストurl
ttps://reportaproblem.apple.com/
apple署名検証
jsonはjwtエンコードされてるので、jwtデコードする必要がある。 その際に、apple証明書で、検証する。アプリ別(bundle_id別)ではなく、どのアプリでも同じ証明書で検証する。
opensslでapple署名検証しながらfirebaseのjwtで、jwtデコードする
composerで、firebase/php-jwtのインストール
composer require firebase/php-jwt
apple署名検証付きデコード処理
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;
function verifyAppleJWSWithFirebaseJWT($signedPayload) {
// 1. JWSのヘッダーを手動で解析(アルゴリズム確認用)
[$headerBase64] = explode('.', $signedPayload);
$header = json_decode(base64_decode($headerBase64), true);
if ($header['alg'] !== 'ES256') {
throw new Exception('Invalid algorithm. Expected ES256');
}
// 2. Appleの証明書チェーンを準備
$leafCert = $header['x5c'][0] ?? null;
if (!$leafCert) {
throw new Exception('Missing x5c certificate chain');
}
// 3. 中間証明書とルート証明書をダウンロード
$intermediateCert = file_get_contents('https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer');
$rootCert = file_get_contents('https://www.apple.com/certificateauthority/AppleRootCA-G3.cer');
// 4. 証明書チェーンを検証
$certificateChain = [
'leaf' => "-----BEGIN CERTIFICATE-----\n{$leafCert}\n-----END CERTIFICATE-----",
'intermediate' => $intermediateCert,
'root' => $rootCert
];
if (!verifyCertificateChain($certificateChain)) {
throw new Exception('Certificate chain validation failed');
}
// 5. firebase/php-jwt でデコード
$decoded = JWT::decode(
$signedPayload,
new Key($certificateChain['leaf'], 'ES256')
);
return $decoded;
}
// 証明書チェーン検証(簡略版)
function verifyCertificateChain($chain) {
// 1. 一時ファイルを安全に作成
$tempDir = sys_get_temp_dir();
$tempFile = tempnam($tempDir, 'cert_');
// 2. 証明書をファイルに書き込み(PEM形式を保証)
file_put_contents($tempFile,
"-----BEGIN CERTIFICATE-----\n" .
chunk_split(base64_encode($chain['leaf']), 64) .
"-----END CERTIFICATE-----\n"
);
// 3. ファイル権限を設定(重要)
chmod($tempFile, 0644);
// 4. 証明書チェーンを構築
$store = openssl_x509_read("file://{$tempFile}");
if ($store === false) {
unlink($tempFile);
throw new Exception('Failed to read certificate');
}
// 5. 目的チェックを実行
$result = openssl_x509_checkpurpose(
"file://{$tempFile}",
X509_PURPOSE_ANY,
[], // 追加のCA証明書は不要(Appleの証明書はシステムに組み込み)
$store
);
// 6. 後処理
unlink($tempFile);
openssl_x509_free($store);
return $result;
}
