Ось докладний підсумок того, як я вирішив це у своїй бібліотеці 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 вика повинна допомогти.
Якщо ви вирішите використовувати 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. Це було зроблено до розбору квитанції та буде детально описано нижче.
- Перевірка, що ідентифікатор пакета, що входить у квитанцію, відповідає вашому ідентифікатору пакету. Вам слід ввести жорсткий код свого ідентифікатора пакета, оскільки, здається, не дуже складно змінити пакет додатка та використовувати іншу квитанцію.
- Перевірка, що версія додатка, включена в квитанцію, відповідає ідентифікатору вашої програми. Вам слід жорстко кодувати версію програми з тих самих причин, що вказані вище.
- Перевірте хеш квитанції, щоб переконатися, що квитанція відповідає поточному пристрою.
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 Root Certificate Authority . Наступний код приймає контейнер 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;
}
Це було зроблено ще на початку, до розбору квитанції.
Перевірка хеша квитанції
Хеш, включений у квитанцію, є 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];
}
І в цьому суть. Я, можливо, щось тут і пропускаю, тому я можу пізніше повернутися до цієї посади. У будь-якому випадку я рекомендую переглянути повний код для отримання більш детальної інформації.