これは、アプリ内購入ライブラリ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 形式で表されます。これには、一般的な情報、検証用のいくつかのフィールド (後で説明します)、および該当する各アプリ内購入の特定の情報が含まれています。
繰り返しになりますが、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;
}
}
}];
消耗品や更新不可能なサブスクリプションなどの特定のアプリ内購入は、領収書に 1 回しか表示されないことに注意してください。購入後すぐにこれらを確認する必要があります (繰り返しますが、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 をハードコードする必要があります。
- レシートに含まれるアプリのバージョンがアプリのバージョン 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 ルート認証局からダウンロードできる Apple Inc. ルート証明書で署名されています。次のコードは、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];
}
そして、それがその要点です。あちこちで何かが足りないかもしれないので、後でこの投稿に戻るかもしれません。いずれにせよ、詳細については完全なコードを参照することをお勧めします。