これは、アプリ内購入ライブラリ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には役立つはずです。
OpenSSLを使用してPKCS7コンテナーを開くことを選択した場合、コードは次のようになります。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形式で表されます。これには、一般的な情報、確認のためのいくつかのフィールド(後で説明します)、および該当する各アプリ内購入の特定の情報が含まれています。
繰り返しますが、OpenSSLは、ASN1を読み取る際に役立ちます。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;
}
領収書の確認
領収書自体を確認すると、次のようになります。
- レシートが有効なPKCS7およびASN1であることを確認します。これはすでに暗黙のうちに行われています。
- 領収書がAppleによって署名されていることの確認。これは、レシートを解析する前に行われました。詳細については、以下で説明します。
- 領収書に含まれるバンドルIDがバンドルIDに対応していることを確認します。アプリバンドルを変更して他の領収書を使用することはそれほど難しくないと思われるため、バンドル識別子をハードコードする必要があります。
- 領収書に記載されているアプリのバージョンがアプリのバージョンIDに対応していることを確認します。上記と同じ理由で、アプリのバージョンをハードコードする必要があります。
- 領収書ハッシュをチェックして、領収書が現在のデバイスに対応していることを確認します。
RMStoreAppReceiptVerificatorからのコードの5つのステップの概要:
- (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のSHA1、レシートに含まれるいくつかの不透明な値、およびバンドルIDです。
これは、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];
}
そして、それはそれの要点です。あちこちで何か欠けている可能性があるので、後でこの投稿に戻るかもしれません。いずれにせよ、詳細については完全なコードを参照することをお勧めします。