Receipt validation fails in test environment

I'm trying to do some validation of the AppStore receipt, but I'm encountering some problems.


I do get the receipt via…


    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];


I manage to parse the data and get the "opaque value" and the SHA1 hash from the receipt. Now I want to validate the hash, following the docs from Apple...


    NSData *opaqueData    /* contains the opaque value from the receipt */
    NSData *sha1HashData  /* contains the SHA1 Hash from the receipt */
    NSData *bundleIdData  /* contains the bundle ID from the receipt */

    NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor];
    uuid_t uuidBytes;
    [uuid getUUIDBytes:uuidBytes];

    NSMutableData *testData = [NSMutableData dataWithBytes:uuidBytes length:sizeof(uuidBytes)];
    [testData appendData:opaqueData];
    [testData appendData:bundleIdData];

    unsigned char result[CC_SHA1_DIGEST_LENGTH];
    CC_SHA1(testData.bytes, testData.length, result);
    NSData *resultData = [NSData dataWithBytes:result length:CC_SHA1_DIGEST_LENGTH];

    BOOL valid = [resultData isEqual:sha1HashData];


But the validation always fails. The SHA1 hash calculated on the device is never matching the one from the receipt.


I did also refresh the receipt...


   SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:0];
   request.delegate = self;
   [request start];


and validate the new receipt after the "requestDidFinish:" delegate method was called, but this will also fail.


I did log out of the AppStore, so when refreshing the receipt the iOS will ask for the name/password for my test user.


Am I doing something wrong? Is this supposed to work when testing and debugging directly from within XCode?


BTW: I've inspected the receipt with some third-party ASN.1 parser, to find out if I've parsed the receipt correctly, and the opaque value and sha1 hash which I've extracted from the receipt do look correct.



I'm also interested in finding out when the App was originally purchased, because I want to provide certain InApp purchases for free for users who had purchased the App recently. I do find the information about the original purchase date in the receipt (and I do get it also when I let the Apple Server validate the receipt via https://buy.itunes.apple.com/verifyReceipt), but this information seems to be not documented by Apple (though I think it was mentioned in a WWDC session). Is there any information about how reliable is this information? Is this always present?


And for Apps installed from the AppStore, is the receipt always present and is it always the current one? Or are their any circumstances where the receipt is missing or needs to be manually refreshed?

Replies

I can't speak for your approach but this code works for me:

    UIDevice *device = [UIDevice currentDevice];
    NSUUID *identifier = [device identifierForVendor];
  
    uuid_t uuid;
    [identifier getUUIDBytes:uuid];
    NSData *guidData = [NSData dataWithBytes:(const void *)uuid length:16];
  
    unsigned char hash[20];
    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);
  
    NSData *computedHashData = [NSData dataWithBytes:hash length:20];
    if (![computedHashData isEqualToData:sha1HashData]) {
          //  return with an error message
    }else{
          //  it worked
   }




>I'm also interested in finding out when the App was originally purchased


Those fields are not documented and not reliable. And the date doesn't matter. Use the original_applciation_version and use consistent values of CFBundleVersion (NOT CFBundleShortVersionString!!!) to test what version the user purchased.


>for Apps installed from the AppStore, is the receipt always present and is it always the current one? Or are their any circumstances where the receipt is missing or needs to be manually refreshed?

Yes, apps installed directly on the device (or is it apps installed from iTunes?) do not have a receipt. Receipts on user's device 1 will not show a recent purchase of an IAP on user's device 2.

Your approach is basicall the same as mine (CC_SHA1 is a short variant for the SHA1_init/Update/Final calls) and has the same result. Does it work for you if XCode directly installs the App on your device so you can actually debug the App?


The problem with the version number is that the receipt contains the build number, not the version number. Though I can probably "live" with this.


OK, this is another question: If an App was purchased in iTunes and then instaled on the device, does it have a receipt?

Yes, this works when installed from Xcode but I have only tested it on receipts after purchasing an IAP. I believe an app can't have a valid receipt when installed from a computer that purchased the app rather than directly from the App Store because Apple can't construct the receipt without access to the device's identifierForVendor.

This might explain the issue, though I do get a receipt from the AppStore when I use the "SKReceiptRefreshRequest" class to refresh the receipt. This will also trigger a password request for the AppStore, so I guess this should work just like it works for IAP. But it does not on my device.

So you are saying that you have an app that you run from Xcode on your device that has not IAPs. When you do an SKReceiptRefreshRequest you do not get a receipt installed on the device. Is that correct?

> So you are saying that you have an app that you run from Xcode on your device that has not IAPs.


Yes, my App does not have any IAP yet.

> When you do an SKReceiptRefreshRequest you do not get a receipt installed on the device. Is that correct?

No. I do get a receipt. But validating the GUID/Sha1 hash will always fail. The other data within the receipt (bundle id, purchase date etc) does look fine.

That's surprising. But again, I have never looked at the receipt before an IAP event occured but I am surprised.


So my code is diferent form yours but 'equivalent' - did you try my code to see if it is actually not equivalent in some way?


You also need the following:


            case 4:
                opaqueData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
                break;
            case 2:
                str_ptr = ptr;
                ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr);
                if(str_type == V_ASN1_UTF8STRING){
                    NSString *temp=[[NSString alloc] initWithBytes:str_ptr length:str_length encoding:NSUTF8StringEncoding];
                    bundleIdData=[[NSData alloc] initWithBytes:(const void *) ptr length:length];
                }
                break;
            case 5:
                hashData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
                break;

>did you try my code to see if it is actually not equivalent in some way?


Yes, I did also try the SHA1_init/Update/Final calls instead of the combined one. But these calls should be equivalent. The "init/update/final" variant is useful if you need to process the data buffer in fragments, the one call can be used if all the data is in one single buffer that can be read directly.


>You also need the following:


I did extract all the data from the receipt.

Perhaps there is something unusual about the way your code sets lengths and other stuff. Try copying, and by that I mean "copying" my code - including the parsing of the receipt and include the bundleIdData and the "20" and the "16" and see if that works.

The constant CC_SHA1_DIGEST_LENGTH is defined as 20 in the header files, and the size of the GUID data is 16, so this is not the issue.


I do use an external ASN.1 Parser to dump the data from the receipt, and it does match the data I extract from the receipt in my App. So I'm pretty sure that the data I'm using in my App is correctly extracted from the receipt.


Just to make sure: this is not about InApp purchase validation. It's only about validating the receipt of the App itself.

It is possible that the difference between my hash agreeing and your hash not agreeing is that I am using a receipt after an IAP purchase and you are using a receipt without an IAP purchase. Or that the difference is somewhere else. But it is also possible that the difference is buried somewhere within one of those assumptions you are making about the line-for-line equivalence between my code and yours. You know what they say when you 'assume' something - you make an '***' out of 'u' and 'me'. Try inserting my exact code and see if it makes any difference.

I'm not sure if it has to do with IAP. Right now there are no IAPs in my App, so I can't check it with IAPs.


And yes, using the single SHA1 call produces the exact same output as the split SHA1-Init/Update/Final calls.

It's not just the split SHA1. You keep referring to a specific difference. There are a number of differences between my code and what you say you are doing. I wonder if any of them might be significant. But if you are sure they are not then...good luck!

My only problem so far is the SHA1 check and I don't see any thing that is different in your code. The different SHA1 calls are equivalent, and using a predefined label like CC_SHA1_DIGEST_LENGTH instead of a number 20 doesn't matter, if the label represents the same number.


Nevertheless, thanks for your input.


Maybe I just go the other "server-based" way and let Apple validate the receipt (via "https://buy.itunes.apple.com/verifyReceipt") instead of doing it locally. At least this works for developement builds as well (in which case the "buy" part of the URL must be replaced by "sandbox").

I tried my code on an app that was not registered on iTunes yet. I was using a device, it doesn't work on the simulator. I was unable to get a receipt even after a SKReceiptRefreshRequest. Then I registered the app on iTunes Connect and added some IAPs. I purchased the IAP and had no problem with the receipt. Then I deleted the app and reloaded it from Xcode and looked for a receipt - it wasn't there. I did a SKReceiptRefreshRequest and the receipt appeared. My code was able to decode the receipt and the hash worked fine.