Handling [[NSBundle mainBundle] appStoreReceiptURL]
Hullo, I am trying to port the solution to the new Receipt management by means of the appStoreReceiptURL. As in https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html now the NSData received is a bundle of a lot of information. So I used the code from the tutorial at: http://www.objc.io/issue-17/receipt-validation.html to unpack it. I managed to install the needed openSSL and some other elements but when I run it I bump into three problems:
- At the inception PKCS7_verify(receiptPKCS7, NULL, store, NULL, NULL, 0); returns 218570875
- The computed hashData and the one taken from the Receipt are different and, finally, what is the critical factor:
- The value I get at the last of several iteration of item 17 (in fact they are all equal) is not recognized by the iTunes Store, that returns in fact 21002
This is my function taking as input the appStoreReceiptURL integrating with MKStoreKit when possible:
-(void) processReceiptUrl:(NSURL*)receiptUrl{ BOOL success=YES; NSArray *downloads = nil;
ifdef __IPHONE_6_0
if([transaction respondsToSelector:@selector(downloads)])
downloads = transaction.downloads;
if([downloads count] > 0) {
[[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];
// We don't have content yet, and we can't finish the transaction
ifndef NDEBUG
NSLog(@"Download(s) started: %@", [transaction description]);
endif
return;
}
endif
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
// Create a memory buffer to extract the PKCS #7 container
BIO *receiptBIO = BIO_new(BIO_s_mem());
BIO_write(receiptBIO, [receiptData bytes], (int) [receiptData length]);
PKCS7 *receiptPKCS7 =d2i_PKCS7_bio(receiptBIO, NULL);
if (!receiptPKCS7) {
NSLog(@"no receipt");
success=NO;
}
// Check that the container has a signature
if (!PKCS7_type_is_signed(receiptPKCS7)) {
NSLog(@"unsigned");
}
// Check that the signed container has actual data
if (!PKCS7_type_is_data(receiptPKCS7->d.sign->contents)) {
success=NO;
}
// Load the Apple Root CA (downloaded from https://www.apple.com/certificateauthority/)
NSURL *appleRootURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"];
NSData *appleRootData = [NSData dataWithContentsOfURL:appleRootURL];
BIO *appleRootBIO = BIO_new(BIO_s_mem());
BIO_set_mem_eof_return(appleRootBIO, 0);
BIO_write(appleRootBIO, (const void *) [appleRootData bytes], (int) [appleRootData length]);
X509 *appleRootX509 = d2i_X509_bio(appleRootBIO, NULL);
// Create a certificate store
X509_STORE *store = X509_STORE_new();
X509_STORE_add_cert(store, appleRootX509);
// Be sure to load the digests before the verification
OpenSSL_add_all_digests();
//OpenSSL_add_all_algorithms();
// Check the signature
int result = PKCS7_verify(receiptPKCS7, NULL, store, NULL, NULL, 0);
if (result != 1) {
NSLog(@"error verify: %lu", ERR_get_error());
//success=NO;
}
ASN1_OCTET_STRING *octets = receiptPKCS7->d.sign->contents->d.data;
const unsigned char *ptr = octets->data;
const unsigned char *end = ptr + octets->length;
const unsigned char *str_ptr;
int type = 0, str_type = 0;
int xclass = 0, str_xclass = 0;
long length = 0, str_length = 0;
// Store for the receipt information
NSString *bundleIdString = nil;
NSString *bundleVersionString = nil;
NSData *bundleIdData = nil;
NSData *hashData = nil;
NSData *opaqueData = nil;
NSDate *expirationDate = nil;
// Date formatter to handle RFC 3339 dates in GMT time zone
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
// Decode payload (a SET is expected)
ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
if (type != V_ASN1_SET) {
success=NO;
}
NSData* iTunesValidationReceipt;
while (ptr < end) {
ASN1_INTEGER *integer;
// Parse the attribute sequence (a SEQUENCE is expected)
ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
if (type != V_ASN1_SEQUENCE) {
success=NO;
}
const unsigned char *seq_end = ptr + length;
long attr_type = 0;
long attr_version = 0;
// Parse the attribute type (an INTEGER is expected)
ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
if (type != V_ASN1_INTEGER) {
success=NO;
}
integer = c2i_ASN1_INTEGER(NULL, &ptr, length);
attr_type = ASN1_INTEGER_get(integer);
ASN1_INTEGER_free(integer);
// Parse the attribute version (an INTEGER is expected)
ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
if (type != V_ASN1_INTEGER) {
success=NO;
}
integer = c2i_ASN1_INTEGER(NULL, &ptr, length);
attr_version = ASN1_INTEGER_get(integer);
ASN1_INTEGER_free(integer);
// Check the attribute value (an OCTET STRING is expected)
ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
if (type != V_ASN1_OCTET_STRING) {
success=NO;
}
NSLog(@"attrtype=%ld", attr_type);
switch (attr_type) {
case 2:
// Bundle identifier
str_ptr = ptr;
ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr);
if (str_type == V_ASN1_UTF8STRING) {
// We store both the decoded string and the raw data for later
// The raw is data will be used when computing the GUID hash
bundleIdString = [[NSString alloc] initWithBytes:str_ptr length:str_length encoding:NSUTF8StringEncoding];
bundleIdData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
}
break;
case 3:
// Bundle version
str_ptr = ptr;
ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr);
if (str_type == V_ASN1_UTF8STRING) {
// We store the decoded string for later
bundleVersionString = [[NSString alloc] initWithBytes:str_ptr length:str_length encoding:NSUTF8StringEncoding];
}
break;
case 4:
// Opaque value
opaqueData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
break;
case 5:
// Computed GUID (SHA-1 Hash)
hashData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
break;
case 17:
str_ptr = ptr;
ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr);
iTunesValidationReceipt = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
NSLog(@"iTunes certificate lenght=%ld %@", str_length, iTunesValidationReceipt);
break;
case 21:
// Expiration date
str_ptr = ptr;
ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr);
if (str_type == V_ASN1_IA5STRING) {
// The date is stored as a string that needs to be parsed
NSString *dateString = [[NSString alloc] initWithBytes:str_ptr length:str_length encoding:NSASCIIStringEncoding];
expirationDate = [formatter dateFromString:dateString];
}
break;
// You can parse more attributes...
default:
break;
}
// Move past the value
ptr += length;
}
// Be sure that all information is present
if (bundleIdString == nil ||
bundleVersionString == nil ||
opaqueData == nil ||
hashData == nil) {
success=NO;
}
NSString *appId=[[NSBundle mainBundle] bundleIdentifier];
NSLog(@"prodName=%@ bundleName=%@", appId, bundleIdString);
if (![bundleIdString isEqualToString:appId]) {
NSLog(@"not my app");
success=NO;
}
// Check the bundle version
NSString *majorVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
NSLog(@"majorVersion=%@ bundleVersion=%@", majorVersion, bundleVersionString);
if (![bundleVersionString isEqualToString:majorVersion]) {
NSLog(@"not current release");
success=NO;
}
UIDevice *device = [UIDevice currentDevice];
NSUUID *uuid = [device identifierForVendor];
NSData *guidData = [NSData dataWithBytes:(const void *)uuid length:16];
unsigned char hash[20];
// Create a hashing context for computation
SHA_CTX ctx;
SHA1_Init(&ctx);
SHA1_Update(&ctx, [guidData bytes], (size_t) [guidData length]);
SHA1_Update(&ctx, [opaqueData bytes], (size_t) [opaqueData length]);
SHA1_Update(&ctx, [bundleIdData bytes], (size_t) [bundleIdData length]);
SHA1_Final(hash, &ctx);
// Do the comparison
NSData *computedHashData = [NSData dataWithBytes:hash length:20];
NSLog(@"%@\n-----%@", computedHashData, hashData);
if (![computedHashData isEqualToData:hashData]) {
NSLog(@"validation fails for hash");
//success=NO;
}
// If an expiration date is present, check it
if (expirationDate) {
NSDate *currentDate = [NSDate date];
if ([expirationDate compare:currentDate] == NSOrderedAscending) {
NSLog(@"volume purchased fails");
success=NO;
}
}
if (success){
[self provideContent:transaction.payment.productIdentifier
forReceipt:iTunesValidationReceipt
hostedContent:downloads];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
else [self failedTransaction:transaction];
EVP_cleanup();
}
Has anyone threaded this way or has some suggestion on how to fix it?
Thanks, Fabrizio