The aim of this guide is to help you finalize the receipt validation process by computing the GUID hash for your app, and comparing it to the hash that’s stored within your receipt itself.
This is a continuation of my receipt validation series. I’m assuming that…
- You’ve prepared to test receipt validation by setting up your app in iTunes Connect.
- You’ve brought in a cryptography library like OpenSSL to be able to work with the PKCS #7 container that acts as the “envelope” for the receipt. Perhaps you’ve even done it the “easy way” with CocoaPods.
- You’ve located and loaded the receipt for validation.
- You’ve extracted the PKCS #7 container.
- You’ve verified the signature on the receipt
- You’ve parsed and decoded the receipt payload
After finishing this guide, you’ll simply need to use the parsed receipt data to perform any app-specific enabling/disabling of features based on the data within a valid receipt. If the receipt is invalid, you’ll need to handle that as well. But all of the relatively difficult work of working with the Open SSL crypto library will be DONE after this guide.
Just want the code? Here you go!
Ready? Let’s do this thing!
Validating the device GUID hash with ReceiptValidator
For this final step, I’ve imagined a single additional function within the
ReceiptValidator struct called
It’ll take in a
ParsedReceipt (review what it looks like), and will to the computation and comparison necessary to verify that the computed hash matches the hash stored in the receipt.
ReceiptValidationError will be thrown if things go wrong within this function. No
ReceiptValidationError being thrown indicates a successful hash computation and comparison.
Here’s the skeleton of the function:
Signaling hash validation failure
Before I get into the implementation of the
validateHash(receipt:) function, let’s define one more
ReceiptValidationError case to describe a bad hash comparison outcome:
Implementing validateHash function
I’ll put the code in front of you, and then do my best to explain my thought process.
Walking through validateHash
First up, I’ve placed three guard statements:
These help guarantee that the rest of the function will have all of the required pieces of data that it needs to fully compute a hash and compare it with what’s in the receipt.
The hash for your app is computed with the following three pieces of information:
1) The app purchaser’s device identifier
2) A piece of “opaque data” found within the receipt
3) Your app’s bundle identifier found within the receipt
The app purchaser’s device identifier is represented as a
uuid_t from the Foundation library. However, to use it with the Open SSL library, we’ll need to be working with an
NSData instance. The following code goes from the
uuid_t instance, to a raw pointer, to an
The last chunk of code within the function actually computes the hash data.
The hash is a SHA1 hash, so what we do here is initialize a SHA1 context, and then update it with all of the ingredients (device identifier, opaque data, and bundle identifier). The hash is finalized, and converted to an instance of
The final guard takes the
computedHashData instance, and compares it to the receipt’s hash data. If it’s identical, validation passes! Otherwise,
ReceiptValidationError.incorrectHash is thrown.
Let’s put it all together into the final
ReceiptValidator struct. Additions are highlighted below:
What to do from here
So what now?
Well, at this stage, if no errors have been thrown, you’ve got a valid receipt.
validateReceipt function returns an instance of
ParsedReceipt so that you can inspect individual Swift-Typed values from the receipt payload to determine what features you should enable or disable, depending on what your needs are.
ReceiptValidationError is thrown at any point along the way, you’ll need to handle them.
Here are a few ideas:
- Implement a grace period, just in case the receipt validation failure occurred for a reason that the user can’t control (e.g. maybe we couldn’t locate the receipt, and requesting a new one failed because Apple was having issues…)
- Disable a feature in your app because receipt validation failed too many times
- Maybe you just need to use the data within the
ParsedReceiptbecause you’re changing the way you monetize your app. Now, instead of making users pay $0.99 for the app, you’re going to give it away for free, but let people buy an in-app purchase to enable “pro” features, or remove ads…whatever. In this case, you may check the
ParsedReceiptto see the original version of the app that your user downloaded. Maybe you want to require users who download your app after version 2.0 to buy an in-app purchase for Feature X, but you want to give it to everyone who already has the app since they may have already paid $0.99 for it, and it’d make them feel ripped off if they had to buy the in-app purchase.
How you handle the parsed receipt data or a receipt validation error is really customizable and specific to your particular app.
The bottom line is that from this point on, you no longer need Open SSL or any additional cryptic, low-level, unsafe pointer-type stuff to finish things out.
I hope this series has been helpful in setting you up to validate receipts locally on a user’s device!
- Preparing to Test Receipt Validation for iOS
- Loading a Receipt for Validation with Swift
- OpenSSL for iOS & Swift the Easy Way
- Extracting a PKCS7 Container for Receipt Validation with Swift
- Receipt Validation – Verifying a Receipt Signature in Swift
- Receipt Validation – Parse and Decode a Receipt with Swift