Skip to content

iOS 7/In-App Purchase/ローカルレシート検証

これの続きです。
iOS 7 + Auto-renewable + TransactionReceipt

これで有効期限が取れたのは取れたのですが(値も正しかった)、これで良いのかわかりません。素直にOpenSSL使って、ローカルレシート確認にする方がいいのかしら…

2013/11/4 追記:
このやり方はAppleのドキュメントに書いてないからダメっぽい。
OpenSSL使ってローカルレシート認証にするのが正解だと思われる。
引き続き検証中。

元の話は、iOSのアプリ内課金(In-App Purchase)でレシートの確認を行う場合、SKPaymentTransaction#transactionReceiptが、iOS 7からDeprecatedになっており、さてどうしたらよいのやら…ということでした。そしてこれに対する回答は、Appleのドキュメントに”超”概要が書いてありました。

レシート検証 プログラミングガイド

P.5
レシートをローカルで検証する
ローカルでの検証には、PKCS #7署名を読み込んで検証するコードと署名されたペイロードを解析し て検証するコードが必要です。

P.7
iOSで(システムが古いために)appStoreReceiptURLメソッドが使用できない場合は、App Storeを使用してSKPaymentTransactionオブジェクトのtransactionReceiptプロパティを 検証するためにフォールバックすることができます。

ふむふむ…「appStoreReceiptURLメソッドが使用できるのならレシートをローカルで検証しなさい」というように読み取れます。何故なら「フォールバックする」=「SKPaymentTransactionオブジェクトのtransactionReceiptプロパティを使う」=「Deprecated」だからです。

ではレシートをローカルで検証するにはどうするか…ドキュメントを読むと次のようにありました。
・ローカルのレシートは、Appleの証明書に署名されたPKCS #7コンテナである
・PKCS #7を読みほどいていくにはOpenSSLを使えば出来るけど、ヒントはドキュメントに書いておく
・まぁそんな感じで実装は開発者にまかせた、頑張れ

お、おう…頑張って実装を…無理や!と思ったら、ここら辺をやってくれている方がいらっしゃいました。ファンタスティック!次の3つを使っていけば、レシートをローカルで検証することが出来ます。

・x2on / OpenSSL-for-iPhone
https://github.com/x2on/OpenSSL-for-iPhone

・rmaddy / VerifyStoreReceiptiOS
https://github.com/rmaddy/VerifyStoreReceiptiOS

・Apple Root Certification Authoirty
http://www.apple.com/certificateauthority/

以下手順。
==========
1. OpenSSL-for-iPhoneの中に build-libssl.sh があるので、ターミナルから叩いて暫く待てば出来上がりです。これを使ったビルドにarが必要なのですが、自分環境にarがなかったので

sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/ar /Applications/Xcode.app/Contents/Developer/usr/bin/ar

こんな感じでシンボリックを張ったら通りました。

2. 1で作ったOpenSSLのlibrary2つ、VerifyStoreReceiptiOSの.hと.m、AppleのROOTサーバ証明書(Apple Inc. Root Certificateの方)を、レシートを検証するアプリのプロジェクトに突っ込みます。AppleのROOTサーバ証明書だけプロジェクトのルートに配置してください。他は任意の場所でOKです。それからプロジェクトの設定について、OpenSSLのヘッダが読み取れるようにSearchPathを足すのと、OpenSSLのライブラリをStatic Linkするように変更してください。

3. VerifyStoreReceipt.m の以下行をアンコメント、さらに自分のアプリに合わせて変更してください。

// const NSString * global_bundleVersion = @"1.0.2";
// const NSString * global_bundleIdentifier = @"com.example.SampleApp";

4.  レシートの検証ロジックを、VerifyStoreReceipt.mを使ったものに変更してください。

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
BOOL result = verifyReceiptAtPath([receiptURL path]);
if (result == YES) {
	NSDictionary *dict = dictionaryWithAppStoreReceipt([receiptURL path]);
}

VerifyStoreReceipt.mは、ローカルレシートがAppleによって署名された “正しい” “改ざんされてない” レシートであることを確認し、さらにペイロードの解析結果をNSDictionaryに詰めて返戻してくれます。素晴らしい。

5. これでローカルのレシートを検証すると、NSDictionary *dictに検証結果が次のように入ってきます。

BundleIdentifier = "<Application Bundle ID>";
BundleIdentifierData = <0c2b7075 72636861 73652e61 6e642e61 64766572 74697369 6e672e53 696e676c 65507572 63686173 65497465 6d>;
Hash = <373fcddd 17a4283a 45b9d7e3 00f2e3ed 45553ac2>;
InApp =     (
            {
        CancelDate = "";
        OriginalPurchaseDate = "2013-10-31T16:13:18Z";
        OriginalTransactionIdentifier = 1000000091908178;
        ProductIdentifier = AutoRenewableProduct;
        PurchaseDate = "2013-11-03T19:35:34Z";
        Quantity = 1;
        SubExpDate = "2013-10-31T16:20:05Z";
        TransactionIdentifier = 1000000091908725;
        WebItemId = 18446744072207895811;
    },
(略)
            {
        CancelDate = "";
        OriginalPurchaseDate = "2013-11-03T19:35:33Z";
        OriginalTransactionIdentifier = 1000000092114639;
        ProductIdentifier = AutoRenewableProduct7;
        PurchaseDate = "2013-11-03T19:35:34Z";
        Quantity = 1;
        SubExpDate = "2013-11-03T19:38:33Z";
        TransactionIdentifier = 1000000092114639;
        WebItemId = 18446744072207895811;
    }
);
OpaqueValue = <6f36aadf d0b10b5a d7b08c39 913a7e14>;
OrigVer = "";
Version = "1.0";
}

アプリのBundle Identifierに紐づくレシートが”InApp”に配列で入ってきます。そして有効期限は”SubExpDate”です。配列中のSubExpDateの最大値が、Auto-renewableの有効期限になります。ここで1つ注意があります。1つのアプリで複数のプロダクトを販売している場合、レシートが混在する可能性があります。この場合、ProductIdentifierの値を見て判断しなければなりません。

NSArray *inAppArray = [dict objectForKey:@"InApp"];
NSTimeInterval expires;
for (NSDictionary *inApp in inAppArray) {
	NSString *expiresDate = [inApp objectForKey:@"SubExpDate"];
	NSTimeInterval e = [self dateFromRFC3339String:expiresDate];
	if (expires < e) {
		expires = e; // SubExpDateの最大値
	}
}

==========

実際に動かしてみて正しい有効期限を取得できました。ちなみに dateFromRFC3339String:expiresDate は、RFC3339の日時文字列をNSTimeIntervalに変換するメソッドです。

さて、レシートをローカルで検証する際、問題になるのは、App Store側でAuto-renewableされた購入については、ローカルにレシートが無いということです。これについては、アプリ側でレシートをもらいにいく必要があります。レシートを貰うために、SKReceiptRefreshRequestを使用します。

receiptRequest = [[SKReceiptRefreshRequest alloc] init];
receiptRequest.delegate = self;
[receiptRequest start];

SKReceiptRefreshRequestの応答は、SKRequestDelegateを介して受け取ることができます。

@interface ViewController : UIViewController <SKRequestDelegate>

SKRequestDelegateのrequestDidFinishの中で、先ほどのローカルレシートを取得する処理とSubExpDateの最大値を取る処理を動かせばOKです。

- (void)requestDidFinish:(SKRequest *)request {
  // 上記の4と5をここにも実装
}

(参考)
・x2on / OpenSSL-for-iPhone
https://github.com/x2on/OpenSSL-for-iPhone
・rmaddy / VerifyStoreReceiptiOS
https://github.com/rmaddy/VerifyStoreReceiptiOS

{ 1 } Trackback

  1. TWorksの試行錯誤記録 : | 2014 年 1 月 1 日 at 01:58 | Permalink

    [...] 苦労したこと 執筆当時にリリースされたiOS 7ですが、アプリ内課金のレシート確認方法がiOS 7からややこしい方法に変わっていることに気がつきました。それの解決方法について当時は日本にも海外にも情報が皆無でしたが、なんとか方法を探り当て記事にすることができました。苦労の過程はコチラに書き残しています。 [...]