すべての記事
詳細解説読了まで 8 分

返金に対応する前に App Store サーバー通知を検証する。webhook URL には誰でも POST できるからです

返金通知は、署名を確認するまではただの HTTPS リクエストにすぎません。App Store サーバー通知と Google Play の RTDN メッセージを検証する方法、各ストアが何に署名しているか、そして偽造された返金や誤って拒否された返金がどれだけの損失になるかを解説します。

温かなランプの光の下、机の上のノートパソコンの横に置かれた真鍮のシーリングスタンプと封をした封筒。App Store サーバー通知を信頼する前に検証すべき理由を表した写真

要点

  • Apple はバージョン 2 の App Store サーバー通知をすべて signedPayload として送信します。これは JSON Web Signature で、ヘッダー、ペイロード、署名という 3 つの Base64URL 部分で構成されています。
  • App Store サーバー通知の JWS ヘッダーには、3 つの証明書からなる x5c 証明書チェーンが含まれます。署名用のリーフ証明書、Apple Worldwide Developer Relations 拡張 1.2.840.113635.100.6.2.1 を持つ Apple の中間証明書、そして Apple のルート証明書です。
  • Apple の App Store Server Library は SignedDataVerifier で通知を検証します。証明書チェーン、バンドル ID、environment、本番環境では appAppleId を確認し、証明書の失効をオンラインで確認することもできます。
  • Google Play のリアルタイム デベロッパー通知は Google Play によって署名されていません。push サブスクリプションでは、Pub/Sub が Authorization ヘッダーに署名済みの OpenID Connect トークンを付与でき、サーバー側でその署名、audience、email クレームを確認します。
  • Google によれば、RTDN が伝えるのは購入の状態が変わったという事実だけです。そのため、返金通知に対する安全な対応は、アクセス権を取り消す前に Google Play Developer API で確認することです。
  • Apple は 17.0.0.0/8 サブネットから App Store サーバー通知を送信しますが、IP 許可リストはエンドポイントに到達できる相手を絞り込むだけです。メッセージが Apple から来たことを証明するのは署名です。
  • 本物のメッセージを誤って拒否する検証処理は、偽造を受け入れる検証処理と同じくらい高くつきます。取りこぼした CONSUMPTION_REQUEST の応答期限は 12 時間、Google Play のチャージバック審査は 24 時間しかないからです。

webhook URL を知った人なら誰でも、そこに返金を送りつけることができます。これが、すべての App Store サーバー通知とすべての Google Play リアルタイム デベロッパー通知の背後にある、不都合な事実です。検証するまでは、そのメッセージは JSON が入った単なる HTTPS POST にすぎません。リクエストに REFUND と書かれていたというだけでサーバーがサブスクリプションを取り消し、クレジットを回収し、アカウントを停止するなら、その権限を curl コマンドを書ける誰にでも渡していることになります。

どちらのストアにも確認の手段があります。Apple は通知そのものに署名するため、Apple のルート証明書さえあれば App Store サーバー通知を検証できます。Google Play は別の方法をとります。メッセージ自体は署名されませんが、Pub/Sub による配信には署名を付けられます。さらに Google は、いずれにせよすべての変更を API で確認するよう求めています。以下では、それぞれが何を証明するのか、そしてどちらの方向に間違えた場合に何を失うのかを説明します。

各ストアが実際に署名しているもの

項目App Store Server Notifications V2Google Play RTDN
メッセージ自体に署名があるかはい、signedPayload は JWSいいえ、data フィールドは base64 エンコードされた JSON
送信元を証明するものApple ルートにつながる x5c 証明書チェーンpush 時の Pub/Sub OIDC トークン、または自分で認証した pull
入れ子の署名付きオブジェクトsignedTransactionInfo と signedRenewalInfo、それぞれが独立した JWSなし
アプリと照合するフィールドbundleId、appAppleId、environmentpackageName
送信元ネットワーク17.0.0.0/8、サンドボックスと本番Google Cloud Pub/Sub
対応前の確認任意、署名付きデータそのものが記録必須、Google は Developer API の呼び出しを求めている
通信方式HTTPS、TLS 1.2 以降HTTPS push、またはクライアント ライブラリによる pull

App Store サーバー通知を検証する方法

Apple のドキュメントでは、signedPayload はピリオドで区切られた 3 つの Base64URL エンコード要素、つまりヘッダー、ペイロード、署名からなる文字列で、JSON Web Signature 仕様に従って署名されていると説明されています。ペイロードのデコードは簡単です。それを信頼するには 4 つの確認が必要です。

  • デコードしたヘッダーから x5c 配列を読み取ります。Apple は順序を明記しています。最初が署名鍵を含む証明書、次が Apple の中間証明書、最後が Apple のルート証明書です。
  • そのチェーンを、Apple の PKI サイトから自分でダウンロードした Apple のルート証明書、実際には Apple Root CA - G3 に対して検証します。メッセージに同梱されて届いたルート証明書は決して信頼しないでください。偽造者はそこにどんな証明書でも入れられるからです。
  • 中間証明書に Apple Worldwide Developer Relations 拡張 1.2.840.113635.100.6.2.1 が含まれていることを確認し、リーフ証明書の公開鍵で JWS 署名を検証します。
  • デコードしたデータを自分のアプリと照合します。対象は bundleId、本番環境での appAppleId、そして environment です。Apple の署名が有効でも、それが別のデベロッパーのアプリのものだったり、本番エンドポイントに届いたサンドボックスの購入だったりすれば、あなたの返金ではありません。

次に、中身についても同じことを行います。data オブジェクトには signedTransactionInfo と signedRenewalInfo が含まれ、それぞれが独自の x5c チェーンを持つ別個の JWS です。外側は検証したのにトランザクションを検証せずに読み取る REFUND ハンドラーは、途中に穴を残しています。

自作せずに Apple のライブラリを使う

Apple は Swift、Java、Python、Node 向けに App Store Server Library を公開しています。その SignedDataVerifier は、ルート証明書、オンライン確認のフラグ、environment、バンドル ID、そして本番環境では appAppleId を受け取ります。Node のソースを見ると、何を強制しているかがわかります。チェーン内の証明書がちょうど 3 つであること、Apple のリーフ拡張 1.2.840.113635.100.6.11.1 と中間証明書の拡張、証明書の有効期間、そしてバンドル ID、アプリ ID、environment の一致です。オンライン確認を有効にすると、リーフ証明書と中間証明書が失効していないかを OCSP レスポンダーに問い合わせます。

木の机の上で、浮き出し印章のある紙の証明書にかざされた宝石用ルーペ。返金メッセージを信頼する前に行うべき念入りな確認を表した写真

IP 許可リストだけで十分か

十分ではありません。Apple は、サーバーで許可リストが必要な場合は 17.0.0.0/8 サブネットを追加するよう案内しており、これはサンドボックスと本番の両方に適用されます。ノイズは減らせますが、メッセージが本物であることは証明できません。また、Apple のネットワークが知らないうちに変わった時点で機能しなくなります。本当の関門は署名チェックにしておきましょう。

Google Play の返金通知を検証する方法

Google Play は、あなたが所有する Pub/Sub トピックに RTDN メッセージを公開します。そのトピックで Pub/Sub パブリッシャーのロールを持つべきなのは、google-play-developer-notifications@system.gserviceaccount.com サービス アカウントだけです。サブスクリプションは次の 2 つの方法のいずれかでメッセージを配信し、それぞれに独自の証明手段があります。

  • pull サブスクリプション: サーバーが Pub/Sub への認証済み接続を開いてメッセージを取得します。公開エンドポイントがないため、第三者が送りつける先がありません。
  • 認証を有効にした push サブスクリプション: Pub/Sub が JSON Web Token に署名し、Authorization ヘッダーに Bearer トークンとして入れて送信します。Google のガイドでは、トークンの署名を検証し、email と audience のクレームが設定した値と一致することを確認するよう求めています。サンプルコードでは email_verified が true であることも必須です。トークンは最大で 1 時間前に発行されたものである可能性があります。
  • 認証なしの push サブスクリプション: その URL に到達できる誰もが、RTDN と同じ形のメッセージを送信できます。

Google が API での確認を求める理由

完全に認証された RTDN であっても、すべての情報を含んでいるわけではありません。Google のセットアップ ガイドは、これらの通知が伝えるのは購入の状態が変わったことだけであり、完全な状態を得るには Google Play Developer API を呼び出す必要があると明言しています。voidedPurchaseNotification の場合、何かを取り消す前に Voided Purchases API でその購入を見つけるか、現在の状態を取得するということです。API 呼び出しは自分のサービス アカウントで認証されるため、その応答は偽造者には偽れません。

偽造された返金や拒否された返金の金銭的なコスト

偽造された返金通知を信じてしまうと、有料の顧客を失います。代金を受け取ったままの相手を締め出すことになり、相手はサポートに問い合わせ、中にはストアに行って、あなたのシステムがすでに認めたふりをした返金を実際に申請する人も出てきます。偽造された購入や更新は逆方向の損失を生みます。存在しない売上に対して、GPU の計算資源、有料のサードパーティ API 呼び出し、ストレージ、クリエイターへの支払いを提供することになるのです。

逆の失敗は目立ちにくく、損失はしばしばより大きくなります。appAppleId の誤り、本番環境でのサンドボックス設定、古いルート証明書などが原因で本物のメッセージを拒否する検証処理は、あなたの証拠が意味を持つ 2 つの返金フローを取りこぼします。Apple は CONSUMPTION_REQUEST から 12 時間以内に消費データを提出するよう求めます。Google は orders.reviewrefund を通じてチャージバック審査に回答する猶予として 24 時間を与えます。そして 2026 年 8 月 3 日以降の注文では、Google Play のチャージバックに負けると、購入価格から Play のサービス手数料を差し引いた額に加え、購入者の銀行が請求する手数料を負担することになります。

失敗何が起きるか何を失うか
偽造された REFUND を受け入れる有料の顧客がアクセスを失うサポートの工数と、自ら招いた本物の返金申請
偽造された購入を受け入れる有料機能が無料で使われる売上のない計算資源、API 呼び出し、ストレージ、支払い
本物の CONSUMPTION_REQUEST を拒否するApple があなたのデータなしで判断するその売上と、提供のためにすでに費やしたすべて
本物のチャージバック審査を拒否するGoogle があなたの証拠なしで判断する2026 年 8 月 3 日以降の注文の購入価格と銀行手数料

検証のバグで期限を逃さないために

  • 検証する前に生のリクエストを保存し、拒否したメッセージが本当に失われることのないようにします。
  • 検証の失敗はすべてアラートにします。偽造メッセージ 1 件はノイズです。すべてのメッセージが一斉に失敗するなら、それはあなた自身のバグです。
  • Apple では、拒否したものを再処理できます。Get Notification History は過去 180 日間の元の signedPayload を返すので、検証処理を修正したうえで、保存または回収したペイロードをもう一度通します。
  • Google では、保留中のチャージバック審査はその通知の中にしか存在しないため、push の audience の設定ミスはそのまま審査の見逃しになります。変更のたびに Play Console の Send Test Message でテストしてください。

RefundHalt は両ストアの通知を検証し、何かを変更する前にストアの API で返金を確認し、CONSUMPTION_REQUEST とチャージバック審査に期限内に回答します。偽のメッセージで顧客を失うことも、本物のメッセージを見逃すこともありません。

よくある質問

App Store サーバー通知はどのように検証すればよいですか?
signedPayload を JSON Web Signature として検証します。ヘッダーから x5c 証明書チェーンを読み取り、Apple の PKI サイトからダウンロードした Apple のルート証明書に対してチェーンを検証し、リーフ証明書の鍵で署名を検証したうえで、bundleId、appAppleId、environment が自分のアプリと一致するかを確認します。Apple の App Store Server Library は、これらすべてを SignedDataVerifier で行います。
App Store サーバー通知の検証にはどの鍵を使いますか?
App Store Connect の鍵は必要ありません。公開鍵は JWS ヘッダー内の x5c チェーンの最初の証明書に含まれており、自分で入手した Apple のルート証明書までチェーンを検証して初めて信頼できます。App 内課金キーは Apple の API を呼び出すためのもので、通知の確認には使いません。
Google Play のリアルタイム デベロッパー通知には署名がありますか?
いいえ。Google Play は RTDN メッセージそのものには署名しません。認証済みの push サブスクリプションでは、Pub/Sub が Authorization ヘッダーで署名済みの OpenID Connect トークンを送るので、それを検証します。また Google は、通知のたびに Google Play Developer API を呼び出して購入の完全な状態を取得するよう求めています。
通知の検証に失敗したとき、サーバーはエラーを返すべきですか?
まず生のリクエストを保存し、それから判断してください。Apple は 40x または 50x の応答を受けると、バージョン 2 の通知を前回の試行から 1、12、24、48、72 時間後に再送します。失敗の原因が自分の設定にある場合は、Get Notification History で 180 日間は元の signedPayload を回収でき、修正後にもう一度検証できます。
Apple の IP 範囲だけを許可している場合でも、通知の検証は必要ですか?
必要です。Apple は許可リストを使うサーバーに 17.0.0.0/8 サブネットを許可するよう求めていますが、これは接続できる相手を制限するだけで、メッセージが本物であることは証明しません。通知を Apple とあなたのアプリに結び付ける確認は、JWS 署名です。

出典と参考資料

RefundHalt

App Store と Google Play の返金を自動処理

続きを読む

次の返金リクエストは、すでに向かってきています。

異議を申し立てられなかった返金について、もう 1 通のサポートメールを読む時間で RefundHalt を設定できます。