torsdag den 5. december 2013

Getting in-app purchases from the App receipt

With the App receipt available it is straight forward to figure out which in-app purchases that have been bought by the user. This blogpost shows how to get that information as part of the receipt validation. Also in this installment I refer to the Receipt Validation Programming Guide.

Checking the receipt after an in-app purchase

A new receipt is automatically downloaded once a SKPaymentTransaction is changing state to SKPaymentTransactionStatePurchased. Thus, it is only a matter of validating and parsing the app-receipt again once that happens. This is checked in the delegate function
-(void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions

Getting the in-app part of the receipt

Below is an extension of listing 1-6 in the guide. The text in bold is the extra statements needed to hold the in-app purchase data size_t i; OCTET_STRING_t *iap[2]; int iap_cnt=0; for (i = 0; i < payload->list.count; i++) { ReceiptAttribute_t *entry; entry = payload->list.array[i]; switch (entry->type) { case 2: bundle_id = &entry->value; break; case 3: bundle_version = &entry->value; break; case 4: opaque = &entry->value; break; case 5: hash = &entry->value; break; case 17: iap[iap_cnt]=&entry->value; iap_cnt ++ ; break; } } The iap[] array should be large enough to hold references to all possible in-app purchases, i.e. the total number of products configured for the app. Each of the OCTET_STRING_t references now hold a new ASN.1 structure that holds the information on the in-app purchase. It is important that the free_struct call is not called before these new structures have been parsed. To parse the structures, a function like the following can be used: static inline void getIAP(OCTET_STRING_t **iap, int iap_cnt){ Payload_t *payload=NULL; asn_dec_rval_t rval; int i,n; for (n=0; n < iap_cnt; n++){ OCTET_STRING_t *this_iap = iap[n]; payload = NULL; rval = asn_DEF_Payload.ber_decoder(NULL,&asn_DEF_Payload,(void **)&payload, this_iap->buf,this_iap->size,0); if (rval.code != RC_OK){ NSLog(@"Failed to decode payload at index %d",n); asn_DEF_Payload.free_struct(&asn_DEF_Payload,payload,0); continue; } for (i=0; i < payload->list.count; i++){ ReceiptAttribute_t *entry; entry = payload->list.array[i]; switch (entry->type) { case AR_IAP_PRODUCT_ID:{ const char *id1 = "com.yourdomain.yourproduct1"; const char *id2 = "com.yourdomain.yourproduct2"; if (strcmp(id1, (char *)entry->value.buf+2)==0){ enableFeature(0); } if (strcmp(id2, (char *)entry->value.buf+2)==0){ enableFeature(1); } } break; default: break; } } asn_DEF_Payload.free_struct(&asn_DEF_Payload,payload,0); } } It is important that the payload is set to NULL for each iteration - otherwise it is not possible to free the payload in each iteration. Note that the OCTET_STRINGs should be parsed from offset 0, while the product IDs are found from offset 2. After this function is called, the full app receipt structure can be freed.

Restoring app purchases

Restoring app purchases using the receipt is a matter of re-fetching and re-parsing the receipt. Fetching an updated receipt can be done as shown here.

søndag den 1. december 2013

Verifying the iOS 7 app receipt

With iOS7, Apple has introduced the app receipt to allow a developer to check if the app is authorized to run on the device it is running, and to track non-consumable in-app purchases. The last part is interesting because it solves a lot of problems in persisting the presence of in-app purchases in a secure way. Also, it automatically transfers in-app purchases to new devices if an App is downloaded there.

Unfortunately, the handling and validation of the receipt is not very well documented - important bits are missing from the documentation. However please read the Receipt Validation Programming Guide as this post refers a lot to that . This is my attempt at clearing up some of the points. The goal for this blog post is not to provide complete code, but to clear out some of the questions one has after reading the documentation

Getting the receipt

A note before we start: REMEMBER to log out of the iTunes store before running this code, and to log in using a test account. You can set up a test account in iTunes connect. And do this every time you run your app until the app is in the iTunes store. Also - check each and every step before continuing - it is very hard to debug the code in big-bang fashion.

The receipt can be loaded from the app bundle by the following code: NSURL *url = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *data = [NSData dataWithContentsOfURL:url]; If data ends up being nil, the receipt is missing. This should never happen in a production environment, but will happen in the sandbox environment used during development.

In a development environment, the receipt can be fetched by code akin to the following code: - (void) getReceipt { self.recreq = [[SKReceiptRefreshRequest alloc] init]; self.recreq.delegate = self; [self.recreq start]; } - (void)requestDidFinish:(SKRequest *)request { if ([request isEqual:self.recreq]){ NSLog(@"Got receipt"); } } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { NSLog(@"Did not get receipt"); } The class containing this code must conform to SKRequestDelegate.

Getting the Apple root certificate

The documentation that is in the Receipt Validation Programming guide is good but incomplete. One crucial part that it leaves out is how to get the Apple root certificate. It turns out that there is no way to get it other than store it locally in your app bundle. So download the file AppleIncRootCertificate.cer and put it in your app bundle. Then the certificate data can be loaded with the following code: NSData *cert = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"]]; This allows you to fill an important missing piece in the receipt validation code, namely the loading of the Apple certificate. Note: In general it may not be a good idea to use objective-c in the code that validates the receipt, as it is much easier to disassemble. If you are paranoid enough, you should stick to core foundation. The same code to get the data is shown here using core foundation. CFBundleRef bundle = CFBundleGetMainBundle(); const char *resname = "AppleIncRootCertificate"; const char *resext = "cer"; CFStringRef rname = CFStringCreateWithCString(NULL, resname, kCFStringEncodingASCII); CFStringRef rext = CFStringCreateWithCString(NULL, resext, kCFStringEncodingASCII); CFURLRef url = CFBundleCopyResourceURL(bundle, rname, rext, NULL); unsigned char fname[1024]; CFURLGetFileSystemRepresentation(url, 0, fname, 1024); uint8_t certdata[2048]; int certlen; int fid = open((const char*)fname, O_RDONLY); certlen=read(fid, certdata, 2048); close(fid); CFRelease(url); CFRelease(rext); CFRelease(rname); And then thank your favorite celestial being that Objective-c was invented.

OpenSSL

Apple has chosen a mechanism for verifying the signature on the receipt that is not supported by their built-in software. Hence you need to include OpenSSL in your project. I have followed the directions on this site and it worked. It took some time to build though, so please be patient. You need to import libcrypto.a into your project and put the include/include/ directory where you build OpenSSL into your search path. These steps should clear the way for the actual verification.

Step 1 - Verifying the signature on the receipt

The code in listing 1-4 in the guide is incomplete. Two things are missing, namely which headers to include: #import <openssl/pkcs7.h> #import <openssl/x509.h> #import <openssl/bio.h> and how to initialize the p7 and Apple variables (note that the Apple variable is initialized differently here than what Apple suggests): b_p7 = BIO_new_mem_buf(receiptdata, receiptlen); ... const unsigned char *cdata = certdata; Apple = d2i_X509(NULL, &cdata, certlen);

If you have not worked with OpenSSL before, you will be bitten by this one: Remember to start your use of the library with OpenSSL_add_all_algorithms() and end it with EVP_cleanup();

The final step is to get the payload out from the receipt. The payload is found in the b_out BIO. As far as I know there is no way of knowing how big it is, except it is smaller than the receipt itself. Hence we use the following construct to get the data out: uint8_t *pld = malloc(receiptlen); int pld_sz = BIO_read(b_out, payload, receiptlen);

Step 2 - Verifying Bundle identifier and version

The next step involves parsing the ASN.1 data found in the payload. I have used the asn1c tool described in the manual. Two things needs to be done after you add the asn1 files to your project:
  • Add the directory with the files to the include path of your project, otherwise they won't build
  • Remove the converter-sample.c file from the project
After that, it is only a matter of using the code in listing 1-6. The manual states that the identifier and version should be checked against a hardcoded version. To get to the actual string, use the buf field of the OCTET_STRING_t. The first two bytes contain type and length, so in order to do the comparison correctly do the following: const char *ref = "1.1"; const char *tst = (const char *)bundle_version->buf+2; if (strcmp(ref,tst)){FAIL}; Since the name and version are hardcoded, remember to get it right.

Step 3 - Verifying hash

Again the code in listing 1-7 is pretty good. The only thing missing is how to get the GUID on iOS. This is fortunately quite easy: NSUUID *vendorID = [[UIDevice currentDevice] identifierForVendor]; uuid_t vendorIDBytes; [vendorID getUUIDBytes:vendorIDBytes];

uuid_t is a pointer to 16 bytes, so that makes it easy to complete listing 1-7.

When doing the comparison, there is no offset of the value, so the correct comparison is

if (memcmp(hash->buf,digest)){FAIL}

Final notes

One thing missing is the clean-up. If you just use the code as listed in the guide, the asn1 structures will leak. At some point you need to free the payload, and that is done like this: asn_DEF_Payload.free_struct(&asn_DEF_Payload, payload,0); In the next installment I will go through how to check the status of the in-app purchases using the receipt.