In App Purchaseのレシートをローカルで検証できるようになった話

  • 175
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

iOS Advend Calender 2013 iOS second stage 6日目担当の@fm_tonakaiです。
普段はWebの会社でiOSアプリの開発を行っています。
初のQiita投稿で緊張しています。
今日はIn App Purchaseのレシートのローカル検証についてちょっと調べたので書きます!

はじめに

iOS7よりSKPaymentTransaction#transactionReceiptがDepricatedになりました。
代わりにNSBundle#appStoreReceiptURLが追加され、
Transactionの処理時にこのAPIを用いてレシート情報を取得することができます。
この情報を用いて今までのようにAppleに問い合わせしなくてもローカル内で
レシートの検証が行えるようになりました。

なりましたが、簡単にできるとは言っていない!

レシートの検証について

公式のドキュメントとして下記のドキュメントが出ています。
レシート検証プログラミングガイド
https://developer.apple.com/jp/devcenter/ios/library/documentation/ValidateAppStoreReceipt.pdf

内容を追っていくと、レシートのデータを証明書で検証して、
中のPayloadを取得して解析してと非常にめんどくさいことを要求されています。
また、このドキュメント自体はヒントしか書いておらず、動くコードは書かれていません。
したがって、内容を理解して自分でコードを書く必要があります。 無理です。

なぜこんなにめんどくさいことになったのか

レシート検証プログラミングガイドによると以下のとおりに書いています。

攻撃者は、アプリケーションのバイナリにパッチを当てる、または検証コードが依存しているオペ
レーティングシステムの基本的なルーチンを改変することによって、検証コードの回避を試みること
があります。このようなタイプの攻撃に対して耐性を得るには、次のものも含めたさまざまなコー
ディングテクニックが必要になります。
● 暗号チェック用のコードは、システムが提供するAPIを使用せずに、インラインで処理します。

システムでAPIを提供するとそこからハックされる危険があるということだそうです。
安全なレシートの検証を行うために、自前で解析しろとのことです。

レシートの解析をする

レシート検証プログラミングガイドのヒントだけでレシート検証を行うには
ゆとりエンジニアには厳しいです。そこで既に実装されているものを探すと以下のものがあります。

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

これを用いるには以下の2つが必要です。

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

Apple Root 証明書
http://www.apple.com/certificateauthority/

1.まずOpenSSL-for-iPhoneをgit cloneしてbuild-libssl.shを実行する

git clone https://github.com/x2on/OpenSSL-for-iPhone
cd OpenSSL-for-iPhone
./build-libssl.sh

build-libssl.shを見るとOpenSSLのソースをダウンロードして
armv7,armv7s,arm64などでビルドしてくれているようです。
ビルドにはしばらく時間がかかります。
完了するとlibディレクトリにlibssl.aとlibcrypto.aが作成されます。

2.Apple Root 証明書をダウンロードする

3.プロジェクトにファイルを追加&設定

  • OpenSSL-for-iPhoneで生成したlibssl.aとlibcrypto.a
  • Apple Root証明書(AppleIncRootCertificate.cer)
  • VeryfyStoreReceipt.h/m
  • Build SettingsのHeader Search Pathsにpath/to/OpenSSL-for-iPhone/includeを追加

4.BundleIDとVersionをハードコーディングする
AppDelegate.mかどこかで以下のコードを書く

NSString *const global_bundleVersion = @"1.0";
NSString *const global_bundleIdentifier = @"com.example.projectname";

5.レシート検証を行う
-paymentQueue: updatedTransactions:内で

    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    if (verifyReceiptAtPath(receiptURL.path)) {
        NSDictionary *receipt = dictionaryWithAppStoreReceipt(receiptURL.path);
        NSLog(@"%@", receipt);
    }

これでレシート検証できるかと思います。

VerifyStoreReceiptiOSで何をしているのか

解析のAPIを共通化すると先程も述べたとおりハックされる危険があります。
実際VerifyStoreReceiptiOSを入れてビルドすると
このコードを使うとハックされる危険があるので
自分で実装することをおすすめするみたいな警告が出ます。

なので勉強のためコードの中身を読んでいくことにします。

必要そうな知識

とりあえず自分はPKCS#7とASN1が謎だったのでとりあえずググってみました。

・PKCS#7
http://eternalwindows.jp/crypto/pkcs7/pkcs701.html
暗号化データや署名データを証明書と共に格納できる形式。RFC2315に詳細が記述されている。

とりあえずレシートデータと証明書を一緒に入れられる形式のデータだよという認識

・ASN1
http://e-words.jp/w/ASN2E1.html
データの構造を定義する言語の一つで、主にコンピュータ間の通信プロトコルを規定するために使われる。ASN.1自体はプロトコルの「ひな型」にあたる。

要はクラス定義のようなものか。

レシート内部の構造としてはこう図解されています。

スクリーンショット 2013-12-04 19.42.17.png

全体がPKCS#7でレシートデータの入っているpayloadの部分がASN1で定義されたフォーマットで
入っているということでしょう。

実際に読んでみる

レシート検証プログラミングガイドでは検証の手順として以下のようになっています。

  1. レシートの位置を特定します。
    レシートが存在しない場合、検証は失敗します。
  2. レシートがAppleによって適切に署名されていることを確認します。
    Appleによる署名がない場合、検証は失敗します。
  3. レシートにあるバンドルIDが、Info.plistファイルにあると予想されるハードコーディングされた定数CFBundleIdentifierの値と一致することを確認します。
    一致しない場合、検証は失敗します。
  4. レシートにあるバージョン文字列が、Info.plistファイルにあると予想されるハードコーディングされたCFBundleShortVersionString定数の値と一致することを確認します。
    一致しない場合、検証は失敗します。
  5. “GUIDのハッシュ値の計算” (9 ページ)の説明に従って、GUIDのハッシュ値を計算します。
    計算結果がレシートにあるハッシュ値と一致しない場合、検証は失敗します。 すべてのテストに合格すると、検証は成功です。

1. レシートの特定

これは簡単です。単純にappStoreReceiptURLの値がnilなら終了です。

2.署名の確認

ここから本格的に検証が始まります。
検証はdictionaryWithAppStoreReceipt関数で行われています。
350行目までが署名の確認のコードのようです。
この辺のことについてはレシート検証プログラミングガイドの検証のヒントとほぼ同じです。
レシートデータをPKCS7として、AppleRoot証明書をX509で読み込み
PKCS7_verify関数で検証しています。
この時最初にOpenSSL_add_all_digests();関数を呼び出さないと検証が失敗してしまうようです。

http://ataugeron.github.io/blog/blog/2013/09/23/app-store-receipt-validation-on-ios-7/

3.4. バンドルIDとバージョン番号のハードコーディングした値がレシートと一致しているか確認する。
これはverifyReceiptAtPath関数の536行目からの最後のチェックです。(GUIDのチェックも行っています)
この段階でレシートの内容を解析している必要があります。

バージョン番号なんかは毎回手で修正しないといけないのでしょうか。。。。
ハードコーディングする理由に関してはコメントにURLが書いています。
http://www.craftymind.com/2011/01/06/mac-app-store-hacked-how-developers-can-better-protect-themselves/
Info.plistの中身なんか簡単に書き換えられるから、そこから値を持ってくるのは危ないということのようです。

5.GUIDのハッシュ値の計算
GUIDのハッシュ値の計算はレシート検証プログラミングガイドでこのように書いています。

GUIDのハッシュ値の計算
OS Xでは、“OS XでのGUIDの取得” (13ページ)で説明された方法を使用してGUIDをフェッチします。
iOSでは、UIDeviceのidentifierForVendorプロパティによって返された値を使用してGUIDを計算します。

ハッシュ値を計算するには、最初にGUID値をオペーク値(タイプ4の属性)とバンドルIDに連結します。UTF-8文字列の解釈や正規化を一切行わずにレシートの生のバイトを使用します。次に連結されたこの一連のバイトに対してSHA-1のハッシュ値を計算します。

さらっと書かれています。。。。

GUIDのハッシュ値の計算は524行目から行っています。
iOSの場合はidentifierForVendorの値を使います。

    unsigned char uuidBytes[16];
    NSUUID *vendorUUID = [[UIDevice currentDevice] identifierForVendor];
    [vendorUUID getUUIDBytes:uuidBytes];
    NSMutableData *input = [NSMutableData data];
    [input appendBytes:uuidBytes length:sizeof(uuidBytes)];
    [input appendData:[receipt objectForKey:kReceiptOpaqueValue]];
    [input appendData:[receipt objectForKey:kReceiptBundleIdentifierData]];

    NSMutableData *hash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH];
    SHA1([input bytes], [input length], [hash mutableBytes]);

レシート情報の解析

署名の確認が終わった段階でペイロードをNSDictionaryに書き換える処理を
351行目から467行目までで行われています。

レシート検証プログラミングガイドではasn1cを使った生成コードを用いての解析の "ヒント" が書かれていますがこのコードでは直接解析をしています。

//350行目
ASN1_OCTET_STRING *octets = p7->d.sign->contents->d.data;
const uint8_t *p = octets->data;

ここでpayloadの始まりのポインタが取れるようです。

それからASN1_get_objectという関数で順繰り値を取っていくという感じになってます。
コードと上記図を見比べるとなんとなく何やってるかがわかる気がします。

解析に必要な情報はレシート検証プログラミングガイドの後半にレシートのフィールドと言う項目でまとめて書いてあります。

おわりに(結局自分でやってないけど)

というわけで、VeryfyReceiptForiOSのコードを見てローカルレシート検証について確認しましたがこれを自力でやるにはなかなか酷な感じです。
とりあえず警告どおりそのまま使わず、関数を分割したり名前を変更したり等このコードを編集する形で利用すればいいのかなと思いました。(小並感)