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.

1 kommentar:

  1. the url of making openssl is expired, I switch to https://gist.github.com/letiemble/6710405/download and works for now.

    SvarSlet