1. ホーム
  2. ios

[解決済み] iOS 7でアプリ内レシートとバンドルレシートをローカルに検証するための完全な解決策

2022-06-25 07:49:04

質問

私は、理論的にはアプリ内またはバンドル領収書を検証するドキュメントやコードをたくさん読みました。

SSL、証明書、暗号化などに関する私の知識がほぼゼロであることを考えると、私が読んだ説明のすべて。 この有望なもののように のように、私が読んだすべての説明は理解するのが難しいものでした。

すべての人がその方法を考えなければならないため、説明が不完全であるとか、ハッカーがパターンを認識・識別してアプリケーションにパッチを当てることができるクラッカーアプリを簡単に作成できるようになるとか。なるほど、これにはある部分まで同意します。このメソッドを変更する」「他のメソッドを変更する」「この変数を難読化する」「これとこれの名前を変更する」など、完全にやり方を説明し、警告を表示することができると思います。

誰か親切に説明してくれる人はいませんか? iOS 7 でレシートとアプリ内課金レシートをローカルに検証する方法。 5 歳の私にもわかるように、上から下へ、わかりやすく教えてください。

ありがとうございます!!!


もしあなたが自分のアプリケーションで動作しているバージョンを持っていて、ハッカーにあなたがどうやったかを見られることを懸念しているなら、ここで公開する前に、単にあなたの機密の方法を変更してください。文字列の難読化、行の順序の変更、ループの実行方法の変更 (for の使用からブロック列挙、またはその逆)、およびそのようなことを行ってください。明らかに、ここに投稿されるかもしれないコードを使用するすべての人は、簡単にハッキングされる危険を冒さないために、同じことをしなければなりません。

どのように解決するのですか?

私のアプリ内課金ライブラリでこれを解決した方法のウォークスルーは次のとおりです。 RMStore . 今回は、レシート全体の検証を含むトランザクションの検証方法を説明します。

一覧で見る

レシートを取得し、取引を確認します。失敗した場合は、レシートをリフレッシュして再試行します。レシートの更新が非同期であるため、検証処理も非同期となります。

から RMStoreAppReceiptVerifier :

RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
const BOOL verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil]; // failureBlock is nil intentionally. See below.
if (verified) return;

// Apple recommends to refresh the receipt if validation fails on iOS
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
    RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
    [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:failureBlock];
} failure:^(NSError *error) {
    [self failWithBlock:failureBlock error:error];
}];

レシートデータの取得

レシートは [[NSBundle mainBundle] appStoreReceiptURL] にあり、実際にはPCKS7コンテナです。私は暗号が苦手なので、このコンテナを開くのにOpenSSLを使いました。他の人は、どうやら純粋に システムフレームワーク .

OpenSSLをプロジェクトに追加することは些細なことではありません。その RMStore wiki が役立つはずです。

PKCS7コンテナを開くためにOpenSSLを使用する場合、コードは次のようになります。から RMAppReceipt :

+ (NSData*)dataFromPKCS7Path:(NSString*)path
{
    const char *cpath = [[path stringByStandardizingPath] fileSystemRepresentation];
    FILE *fp = fopen(cpath, "rb");
    if (!fp) return nil;

    PKCS7 *p7 = d2i_PKCS7_fp(fp, NULL);
    fclose(fp);

    if (!p7) return nil;

    NSData *data;
    NSURL *certificateURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"];
    NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL];
    if ([self verifyPKCS7:p7 withCertificateData:certificateData])
    {
        struct pkcs7_st *contents = p7->d.sign->contents;
        if (PKCS7_type_is_data(contents))
        {
            ASN1_OCTET_STRING *octets = contents->d.data;
            data = [NSData dataWithBytes:octets->data length:octets->length];
        }
    }
    PKCS7_free(p7);
    return data;
}

検証の詳細は後述します。

領収書フィールドの取得

領収書はASN1形式で表されます。一般的な情報、検証用のフィールド(後で説明します)、該当するアプリ内課金の具体的な情報が含まれています。

ASN1 の読み取りに関しては、ここでも OpenSSL が役に立ちます。以下から RMAppReceipt から、いくつかのヘルパーメソッドを使って

NSMutableArray *purchases = [NSMutableArray array];
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
    const uint8_t *s = data.bytes;
    const NSUInteger length = data.length;
    switch (type)
    {
        case RMAppReceiptASN1TypeBundleIdentifier:
            _bundleIdentifierData = data;
            _bundleIdentifier = RMASN1ReadUTF8String(&s, length);
            break;
        case RMAppReceiptASN1TypeAppVersion:
            _appVersion = RMASN1ReadUTF8String(&s, length);
            break;
        case RMAppReceiptASN1TypeOpaqueValue:
            _opaqueValue = data;
            break;
        case RMAppReceiptASN1TypeHash:
            _hash = data;
            break;
        case RMAppReceiptASN1TypeInAppPurchaseReceipt:
        {
            RMAppReceiptIAP *purchase = [[RMAppReceiptIAP alloc] initWithASN1Data:data];
            [purchases addObject:purchase];
            break;
        }
        case RMAppReceiptASN1TypeOriginalAppVersion:
            _originalAppVersion = RMASN1ReadUTF8String(&s, length);
            break;
        case RMAppReceiptASN1TypeExpirationDate:
        {
            NSString *string = RMASN1ReadIA5SString(&s, length);
            _expirationDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
    }
}];
_inAppPurchases = purchases;

アプリ内課金の取得

各アプリ内課金もASN1になっています。これをパースするのは、一般的なレシート情報をパースするのと非常に似ています。

以下より RMAppReceipt のように、同じヘルパーメソッドを使って

[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
    const uint8_t *p = data.bytes;
    const NSUInteger length = data.length;
    switch (type)
    {
        case RMAppReceiptASN1TypeQuantity:
            _quantity = RMASN1ReadInteger(&p, length);
            break;
        case RMAppReceiptASN1TypeProductIdentifier:
            _productIdentifier = RMASN1ReadUTF8String(&p, length);
            break;
        case RMAppReceiptASN1TypeTransactionIdentifier:
            _transactionIdentifier = RMASN1ReadUTF8String(&p, length);
            break;
        case RMAppReceiptASN1TypePurchaseDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _purchaseDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
        case RMAppReceiptASN1TypeOriginalTransactionIdentifier:
            _originalTransactionIdentifier = RMASN1ReadUTF8String(&p, length);
            break;
        case RMAppReceiptASN1TypeOriginalPurchaseDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
        case RMAppReceiptASN1TypeSubscriptionExpirationDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
        case RMAppReceiptASN1TypeWebOrderLineItemID:
            _webOrderLineItemID = RMASN1ReadInteger(&p, length);
            break;
        case RMAppReceiptASN1TypeCancellationDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _cancellationDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
    }
}]; 

消耗品や更新不可能なサブスクリプションなど、特定のアプリ内購入は、レシートに一度だけ表示されることに注意する必要があります。購入後すぐにこれらを確認する必要があります(ここでもRMStoreがお手伝いします)。

一目でわかる検証

これでレシートとそのアプリ内課金からすべてのフィールドを取得できました。まず、レシートそのものを検証し、次にレシートに取引の商品が含まれているかどうかを簡単に確認します。

以下は、冒頭で呼び出したメソッドです。以下は、冒頭で呼び出したメソッドです。 RMStoreAppReceiptVerificator(レシートベリフィケータ :

- (BOOL)verifyTransaction:(SKPaymentTransaction*)transaction
                inReceipt:(RMAppReceipt*)receipt
                           success:(void (^)())successBlock
                           failure:(void (^)(NSError *error))failureBlock
{
    const BOOL receiptVerified = [self verifyAppReceipt:receipt];
    if (!receiptVerified)
    {
        [self failWithBlock:failureBlock message:NSLocalizedString(@"The app receipt failed verification", @"")];
        return NO;
    }
    SKPayment *payment = transaction.payment;
    const BOOL transactionVerified = [receipt containsInAppPurchaseOfProductIdentifier:payment.productIdentifier];
    if (!transactionVerified)
    {
        [self failWithBlock:failureBlock message:NSLocalizedString(@"The app receipt doest not contain the given product", @"")];
        return NO;
    }
    if (successBlock)
    {
        successBlock();
    }
    return YES;
}

領収書を確認する

レシートの検証自体は煮詰まっている。

  1. 領収書が有効な PKCS7 および ASN1 であることを確認する。これはすでに暗黙のうちに行われました。
  2. レシートが Apple によって署名されていることを確認します。これはレシートを解析する前に行われたもので、以下で詳しく説明します。
  3. レシートに含まれるバンドル識別子が、自分のバンドル識別子と一致するかどうかを確認します。アプリのバンドルを変更して他のレシートを使用することはそれほど難しくないようなので、バンドル識別子をハードコードしておくとよいでしょう。
  4. レシートに含まれるアプリのバージョンが、アプリのバージョン識別子に対応していることを確認します。上記と同じ理由により、アプリのバージョンはハードコードする必要があります。
  5. レシートのハッシュをチェックし、レシートが現在のデバイスに対応していることを確認します。

5つのステップを高レベルでコード化します。 RMStoreAppReceiptVerificator(レシート検証ツール :

- (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt
{
    // Steps 1 & 2 were done while parsing the receipt
    if (!receipt) return NO;   

    // Step 3
    if (![receipt.bundleIdentifier isEqualToString:self.bundleIdentifier]) return NO;

    // Step 4        
    if (![receipt.appVersion isEqualToString:self.bundleVersion]) return NO;

    // Step 5        
    if (![receipt verifyReceiptHash]) return NO;

    return YES;
}

ステップ2とステップ5を掘り下げてみましょう。

領収書の署名を確認する

データを抽出したときに、レシートの署名の検証をちらっと見てみました。レシートには Apple Inc. ルート証明書を使用して署名されています。 Apple ルート証明書機関 . 以下のコードでは、PKCS7コンテナとルート証明書をデータとして受け取り、両者が一致するかどうかをチェックしています。

+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{ // Based on: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW17
    static int verified = 1;
    int result = 0;
    OpenSSL_add_all_digests(); // Required for PKCS7_verify to work
    X509_STORE *store = X509_STORE_new();
    if (store)
    {
        const uint8_t *certificateBytes = (uint8_t *)(certificateData.bytes);
        X509 *certificate = d2i_X509(NULL, &certificateBytes, (long)certificateData.length);
        if (certificate)
        {
            X509_STORE_add_cert(store, certificate);

            BIO *payload = BIO_new(BIO_s_mem());
            result = PKCS7_verify(container, NULL, store, NULL, payload, 0);
            BIO_free(payload);

            X509_free(certificate);
        }
    }
    X509_STORE_free(store);
    EVP_cleanup(); // Balances OpenSSL_add_all_digests (), per http://www.openssl.org/docs/crypto/OpenSSL_add_all_algorithms.html

    return result == verified;
}

これは最初に戻って、レシートが解析される前に行われました。

レシートハッシュの検証

領収書に含まれるハッシュは、デバイスID、領収書に含まれる何らかの不透明な値、バンドルIDのSHA1です。

iOSではこのようにレシートのハッシュを確認します。以下より RMAppReceipt :

- (BOOL)verifyReceiptHash
{
    // TODO: Getting the uuid in Mac is different. See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
    NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor];
    unsigned char uuidBytes[16];
    [uuid getUUIDBytes:uuidBytes];

    // Order taken from: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
    NSMutableData *data = [NSMutableData data];
    [data appendBytes:uuidBytes length:sizeof(uuidBytes)];
    [data appendData:self.opaqueValue];
    [data appendData:self.bundleIdentifierData];

    NSMutableData *expectedHash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH];
    SHA1(data.bytes, data.length, expectedHash.mutableBytes);

    return [expectedHash isEqualToData:self.hash];
}

というのが大まかなところです。何か見落としているかもしれないので、後でこの記事に戻ってくるかもしれません。いずれにせよ、詳細については、完全なコードを閲覧することをお勧めします。