Voici une procédure pas à pas de la façon dont j'ai résolu ce problème dans ma bibliothèque d'achat intégré RMStore . Je vais vous expliquer comment vérifier une transaction, ce qui comprend la vérification de l'ensemble du reçu.
En un coup d'oeil
Obtenez le reçu et vérifiez la transaction. En cas d'échec, actualisez le reçu et réessayez. Cela rend le processus de vérification asynchrone car l'actualisation de la réception est asynchrone.
À partir de 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];
}];
Obtenir les données de reçu
Le reçu est dans [[NSBundle mainBundle] appStoreReceiptURL]
et est en fait un conteneur PCKS7. Je suis nul en cryptographie, alors j'ai utilisé OpenSSL pour ouvrir ce conteneur. D'autres l'ont apparemment fait uniquement avec des cadres système .
Ajouter OpenSSL à votre projet n'est pas anodin. Le wiki RMStore devrait vous aider.
Si vous choisissez d'utiliser OpenSSL pour ouvrir le conteneur PKCS7, votre code pourrait ressembler à ceci. De 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;
}
Nous entrerons dans les détails de la vérification plus tard.
Obtenir les champs de réception
Le reçu est exprimé au format ASN1. Il contient des informations générales, certains champs à des fins de vérification (nous y reviendrons plus tard) et des informations spécifiques sur chaque achat via l'application applicable.
Encore une fois, OpenSSL vient à la rescousse lorsqu'il s'agit de lire ASN1. À partir de RMAppReceipt , en utilisant quelques méthodes d'assistance:
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;
Obtenir les achats intégrés
Chaque achat intégré est également en ASN1. L'analyse est très similaire à l'analyse des informations générales de réception.
À partir de RMAppReceipt , en utilisant les mêmes méthodes d'assistance:
[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;
}
}
}];
Il est à noter que certains achats intégrés, tels que les consommables et les abonnements non renouvelables, n'apparaîtront qu'une seule fois sur le reçu. Vous devez les vérifier juste après l'achat (encore une fois, RMStore vous aide avec cela).
Vérification en un coup d'œil
Maintenant, nous avons tous les champs du reçu et tous ses achats intégrés. Nous vérifions d'abord le reçu lui-même, puis nous vérifions simplement si le reçu contient le produit de la transaction.
Voici la méthode que nous avons rappelée au début. Depuis 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;
}
Vérification du reçu
La vérification du reçu lui-même se résume à:
- Vérification de la validité du reçu PKCS7 et ASN1. Nous l'avons déjà fait implicitement.
- Vérifier que le reçu est signé par Apple. Cela a été fait avant l'analyse du reçu et sera détaillé ci-dessous.
- Vérifier que l'identifiant du bundle inclus dans le reçu correspond à votre identifiant du bundle. Vous devez coder en dur l'identifiant de votre bundle, car il ne semble pas très difficile de modifier votre bundle d'applications et d'utiliser un autre reçu.
- Vérifier que la version de l'application incluse dans le reçu correspond à l'identifiant de la version de votre application. Vous devez coder en dur la version de l'application, pour les mêmes raisons indiquées ci-dessus.
- Vérifiez le hachage du reçu pour vous assurer que le reçu correspond à l'appareil actuel.
Les 5 étapes du code de haut niveau, à partir de 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;
}
Explorons les étapes 2 et 5.
Vérification de la signature du reçu
À l'époque où nous avons extrait les données, nous avons jeté un coup d'œil sur la vérification de la signature du reçu. Le reçu est signé avec le certificat racine Apple Inc., qui peut être téléchargé à partir de l' autorité de certification racine Apple . Le code suivant prend le conteneur PKCS7 et le certificat racine en tant que données et vérifie s'ils correspondent:
+ (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;
}
Cela a été fait au début, avant que le reçu ne soit analysé.
Vérification du hachage du reçu
Le hachage inclus dans le reçu est un SHA1 de l'identifiant de l'appareil, une valeur opaque incluse dans le reçu et l'identifiant du bundle.
C'est ainsi que vous vérifieriez le hachage du reçu sur iOS. De 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];
}
Et c'est l'essentiel. Il me manque peut-être quelque chose ici ou là, alors je reviendrai peut-être sur ce post plus tard. Dans tous les cas, je recommande de parcourir le code complet pour plus de détails.