facebook twitter hatena line email

「Php/アプリストア連携/返金API/AppStore」の版間の差分

提供: 初心者エンジニアの簡易メモ
移動: 案内検索
(openssl_x509_store_add_certが使えない場合はコマンドで実行する)
(opensslでapple署名検証しながらfirebaseのjwtで、jwtデコードする)
行101: 行101:
 
// 証明書チェーン検証(簡略版)
 
// 証明書チェーン検証(簡略版)
 
function verifyCertificateChain($chain) {
 
function verifyCertificateChain($chain) {
     // 1. 一時ファイルを安全に作成
+
     // 1. 証明書をメモリ上で読み込み
     $tempDir = sys_get_temp_dir();
+
     $cert = openssl_x509_read($chain['leaf']);
    $tempFile = tempnam($tempDir, 'cert_');
+
     if ($cert === false) {
   
+
         throw new Exception('Memory read failed: ' . openssl_error_string());
    // 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. 目的チェックを実行
+
     // 2. 一時ファイルを作成(PHP 8.0+ の仕様に対応)
 +
    $tempFile = tempnam(sys_get_temp_dir(), 'cert_');
 +
    file_put_contents($tempFile, $chain['leaf']);
 +
   
 +
    // 3. 証明書検証(引数仕様に準拠)
 
     $result = openssl_x509_checkpurpose(
 
     $result = openssl_x509_checkpurpose(
         "file://{$tempFile}",
+
         $tempFile,         // string 型で渡す
 
         X509_PURPOSE_ANY,
 
         X509_PURPOSE_ANY,
         [], // 追加のCA証明書は不要(Appleの証明書はシステムに組み込み)
+
         [],                 // CAファイル(空でシステムデフォルトを使用)
         $store
+
         $tempFile        // 検証対象証明書
 
     );
 
     );
 
+
   
     // 6. 後処理
+
     // 4. リソース解放
 +
    openssl_x509_free($cert);
 
     unlink($tempFile);
 
     unlink($tempFile);
     openssl_x509_free($store);
+
      
 
+
     return (bool)$result;
     return $result;
+
 
}
 
}
 
</pre>
 
</pre>

2025年6月6日 (金) 19:36時点における版

appleのstorekitの返金のドキュメント

https://developer.apple.com/jp/documentation/storekit/in-app_purchase/handling_refund_notifications/

https://developer.apple.com/documentation/AppStoreServerNotifications/unified_receipt/Latest_receipt_info-data.dictionary

ストアからの通知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. 証明書をメモリ上で読み込み
    $cert = openssl_x509_read($chain['leaf']);
    if ($cert === false) {
        throw new Exception('Memory read failed: ' . openssl_error_string());
    }

    // 2. 一時ファイルを作成(PHP 8.0+ の仕様に対応)
    $tempFile = tempnam(sys_get_temp_dir(), 'cert_');
    file_put_contents($tempFile, $chain['leaf']);
    
    // 3. 証明書検証(引数仕様に準拠)
    $result = openssl_x509_checkpurpose(
        $tempFile,          // string 型で渡す
        X509_PURPOSE_ANY,
        [],                 // CAファイル(空でシステムデフォルトを使用)
        $tempFile         // 検証対象証明書
    );
    
    // 4. リソース解放
    openssl_x509_free($cert);
    unlink($tempFile);
    
    return (bool)$result;
}