コネヒト開発者ブログ

コネヒト開発者ブログ

In App Purchaseの VerifyReceipt APIからApp Store Server APIに移行しました

はじめに

こんにちは! otukutun.bsky.social です。 今回はIn-App PurchaseのサーバーサイドAPIを、VerifyReceipt API から App Store Server API に移行した経験を共有できればと思います。

WWDC2023 で、VerifyReceipt APIのdeprecated化が発表され、 WWDC2024 では従来のStoreKit1がdeprecated化と宣言され、これまでのAPI群は「original API for In-App Purchase」に改名されました。まだ廃止日時などは明言されていないと思いますが、今後はApp Store Server APIやStoreKit 2に移行しなけばいけません。

弊社が提供している ママリプレミアム のサブスクリプションはiOSではIn-App Purchaseを使用し提供しています。今回はVerifyReceipt APIからApp Store Server APIに移行した際の手順や実装について解説していきます。主にPHPでの実装方法に焦点を当てています。

なお、この記事はコネヒトアドベントカレンダー 16日目の記事になります。

App Store Server APIの概要

App Store Server APIは、Appleが提供する新しい課金情報管理APIです。用途ごとに様々なAPIが提供され、購読情報だけでなく返金情報も取得できるようになりました。今回は GET /inApps/v1/subscriptions/{transactionId} を使用し、トランザクションIDをもとに最新の課金情報を取得することにしました。

PHPでの実装

JWSの取り扱い

App Store Server APIでは、レスポンスにJWS(JSON Web Signature)が返却されそれを検証することで購入情報の正当性を確認できるようになりました。AppleはPHP向けの公式ライブラリを提供しておらず、PHPでのJWT検証ライブラリとしてメジャーな firebase/php-jwt はx5c形式をサポートしていないため、PHP標準のOpenSSL 関数と組み合わせることで対応しました。

  1. firebase/php-jwt ライブラリを利用
    • JWTのデコードと検証を行います
  2. PHP標準の openssl ラッパー関数を活用
    • 証明書の有効性や署名の検証を行います

証明書の有効性の検証

Appleから返却されるJWSには、証明書情報(x5cフィールド)が含まれています。これを使用して、以下の手順で証明書の有効性を確認します。

  1. リーフ証明書、中間証明書、ルート証明書を検証
  2. 各証明書の情報(有効期限や発行者など)を検証
  3. JWSの検証

以下は具体的なコード例です。

1. リーフ証明書、中間証明書、ルート証明書を検証

<?php

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

function toPem($certificate): string {
    return join("\\n", [
        "-----BEGIN CERTIFICATE-----",
        trim(chunk_split($certificate, 64)),
        "-----END CERTIFICATE-----",
    ]);
}

// サンプルJWS(ChatGPTに生成してもらいました
$jws = 'eyJhbGciOiJFUzI1NiIsImtpZCI6IjEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJleGFtcGxleDVjIn0.eyJ0cmFuc2FjdGlvbl9pZCI6IjEwMDAwMDAxMjM0NTY3ODkiLCJvcmlnaW5hbF90cmFuc2FjdGlvbl9pZCI6IjEwMDAwMDAxMjM0NTY3ODkiLCJ3ZWJfb3JkZXJfbGluZV9pdGVtX2lkIjoiMTAwMDAwMDEyMzQ1Njc4OSIsInByb2R1Y3RfaWQiOiJjb20uZXhhbXBsZS5wcm9kdWN0Iiwic3Vic2NyaXB0aW9uX2dyb3VwX2lkZW50aWZpZXIiOiIxMjM0NTY3OCIsInB1cmNoYXNlX2RhdGUiOiIyMDI0LTExLTAxVDEyOjM0OjU2WiIsImV4cGlyZXNfZGF0ZSI6IjIwMjUtMTEtMDFUMTI6MzQ6NTZaIiwiaXNfaW5fYmlsbGluZ19yZXRyeV9wZXJpb2QiOmZhbHNlLCJlblZpcm9ubWVudCI6IlByb2R1Y3Rpb24ifQ.SIGxEcD9EXAMPLESIGNATURE';

// JWSのデコード
list($header64, $body64, $cryptob64) = explode('.', $jws);

// ヘッダー情報の取得
$headerText = JWT::urlsafeB64Decode($header64);
$header = JWT::jsonDecode($headerText);

// 証明書を取得してPEM形式に変換
$leafCertificate = toPem($header->x5c[0]);
$intermediateCertificate = toPem($header->x5c[1]);
$rootCertificate = toPem($header->x5c[2]);
// URLは適切なものを設定してください
$rootCertificateFromApple = toPem(JWT::urlsafeB64Encode(file_get_contents('APPLE_ROOT_G3_CERTIFICATE_URL')));

// リーフ証明書の検証
$result = openssl_x509_verify($leafCertificate, $intermediateCertificate);
if ($result !== 1) {
    throw new Exception('リーフ証明書の検証に失敗しました');
}

// 中間証明書の検証
$result = openssl_x509_verify($intermediateCertificate, $rootCertificate);
if ($result !== 1) {
    throw new Exception('中間証明書の検証に失敗しました');
}

// ルート証明書の検証
$result = openssl_x509_verify($rootCertificate, $rootCertificateFromApple);
if ($result !== 1) {
    throw new Exception('ルート証明書の検証に失敗しました');
}

リーフ証明書、中間証明書はJWSのheaderに付与されているのでそれらを使って検証します。PEM形式に変換し、openssl_x509_verifyを使えば検証できます。1なら有効、0なら無効なものになります。

ルート証明書の検証はルート証明書自体で行います。検証用の証明書はアップルのサイトからダウンロードできます。CER形式なので、PEM形式に変換する必要があります。

2. 各証明書の情報(有効期限や発行者など)を検証

<?php

// 各証明書情報の検証
$leafInfo = openssl_x509_parse($leafCertificate);
$intermediateInfo = openssl_x509_parse($intermediateCertificate);
$rootInfo = openssl_x509_parse($rootCertificate);

// 検証する情報は項目が多数あるのでここでは省略します

openssl_x509_parseを使って証明書の情報が取得できるので、各証明書の有効期限やOID、issuer情報などを検証すると良いと思います。有効期限はJWSに含まれているsignedDateを使って検証できます。

3. JWSの検証

<?php

$publicKey = openssl_pkey_get_public($leafCertificate);
$result = JWT::decode($jws, new Key($publicKey, $header->alg));
if (!$result) {
    throw new Exception('JWSの署名検証に失敗しました');
}

最後に、JWS自体の署名を検証します。これで検証が無事完了されれば、Transaction情報 を使って課金ステータスなどを更新すれば良いと思います。

おわりに

App Store Server APIは、従来のVerifyReceipt APIに比べてモダンな仕様であり、より強力な課金管理機能を提供されています。実装としてはライブラリが提供されていない言語では各自で対応しなければいけないことはいくつかありますが、PHPでも firebase/php-jwt とPHP標準のOpenSSL関数を使えば、対応は難しくないかなと感じました。またこの実装に加えて、OCSPやCRLなどのオンラインでの証明書期限切れの仕組みを導入することでより強固な検証プロセスを構築できると思います。

実際にご自身で実装する際にはApple提供のライブラリの実装やWWDCの動画を実際に見られることをお勧めします。ReveneuCat提供の記事はStoreKit 2について最初に理解するのにとても役立ちました。こちらが主に参考にした動画やページになります。

今回の記事が、同じ課題に直面している開発者の参考になれば幸いです!