1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

X.509 を検証せずに App Store のサブスクを確認する — Deno Edge Function で App Store Server API を叩く

1
Posted at

個人開発アプリの freemium サブスク(Plus)を、RevenueCat を使わず Supabase の Deno Edge Function で検証した記録です。鍵は「証明書チェーンを検証しない」こと。

制約:Deno では x5c チェーン検証ができない

前提として、別記事に書いた壁があります——Supabase の Deno Edge ランタイムでは Apple の x5c 証明書チェーン検証が動きません(node:cryptoX509Certificate@peculiar/x509pkijs がいずれも boot か実行時に死ぬ)。サブスク検証で Apple の JWS 署名チェーンを検証しようとすると、まっすぐこの壁に戻ります。

チェーン検証なしで「検証済みの権利状態」を得る道は2つ:

  1. RevenueCat(マネージド。Apple のチェーン検証を肩代わり。ただしベンダーと売上1%が増える)
  2. 自前の Edge Function で App Store Server API を叩き、TLS を信頼する

(2) を第一候補にしました。Deno に要求するのは EC 署名と base64 デコードだけで、どちらもランタイム内で確実に動くからです。

自分で署名する「リクエスト」JWT

検証するのではなく、こちらから Apple を呼ぶための JWT を ES256 で署名します。

import * as jose from "npm:jose@5";

const key = await jose.importPKCS8(p8, "ES256"); // In-App Purchase キーの .p8
const jwt = await new jose.SignJWT({ bid: bundleId })
  .setProtectedHeader({ alg: "ES256", kid: keyId, typ: "JWT" })
  .setIssuer(issuerId)
  .setIssuedAt()
  .setExpirationTime("50m")            // ≤60分。1トークンを使い回してよい
  .setAudience("appstoreconnect-v1")
  .setJti(nonce)
  .sign(key);

ハマりどころ:鍵は In-App Purchase キー.p8 を使うこと(通常の App Store Connect API キーだと NOT_AUTHORIZED が返る)。

肝:TLS を信頼して、チェーンは検証しない

GET /inApps/v1/subscriptions/{transactionId} を TLS で叩くと、signedTransactionInfo という JWS(x5c ヘッダ付き)が返ります。これを——payload セグメントだけデコードして、チェーン検証はスキップします。

const info = jose.decodeJwt(signedTransactionInfo); // 署名チェーン検証なし

根拠:Apple の既知ホストへ こちらから TLS を張った ので、チャネルが発信元を認証している。x5c チェーンの再検証は、まさに Deno で動かない作業であり、すでにセキュリティ上クリティカルでないデータ(権利状態。端末側の StoreKit 2 とサーバの per-user 行が backstop)への二重防御にすぎません。トレードオフとして受け入れました。

Server Notifications は信用しない

通知の push ペイロードは、URL を知った者なら誰でも偽造できます。なので通知は無視するか、「いま再ポーリングしろ」の合図としてだけ扱い、真実は常に TLS 越しのポーリングです。

実際に動いた(スパイク)

ライブの Deno Edge ランタイムで端から端まで確認:jose.p8 を読み ES256 で署名 → サンドボックスの App Store Server API を TLS で呼ぶ → 存在しない transaction id に 4040010 Transaction id not found(不正な形式の id なら 4000006。いずれも認証は通って、リクエスト検証まで到達したというお決まりの成功シグナル)。x5c チェーン検証はどこでも不要でした。

すべての consumer は1つの resolveEntitlement() シームから権利状態を読むので、もし将来詰まっても RevenueCat への切り替えは「バックエンドの差し替え」であって書き直しではありません。

アプリ本体(iPhone・基本無料):APP STORE

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?