Finalizing Receipt Validation in Swift – Computing a GUID Hash

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…

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 validateHash(receipt:).

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.

A 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:

1
2
3
4
5
6
7
fileprivate func validateHash(receipt: ParsedReceipt) throws {
    // Make sure that the ParsedReceipt instances has non-nil values needed for hash comparison

    // Compute the hash for your app & device

    // Compare the computed hash with the receipt's hash
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum ReceiptValidationError : Error {
    case couldNotFindReceipt
    case emptyReceiptContents
    case receiptNotSigned
    case appleRootCertificateNotFound
    case receiptSignatureInvalid
    case malformedReceipt
    case malformedInAppPurchaseReceipt
    case incorrectHash
}

Implementing validateHash function

I’ll put the code in front of you, and then do my best to explain my thought process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fileprivate func validateHash(receipt: ParsedReceipt) throws {
    // Make sure that the ParsedReceipt instances has non-nil values needed for hash comparison
    guard let receiptOpaqueValueData = receipt.opaqueValue else { throw ReceiptValidationError.incorrectHash }
    guard let receiptBundleIdData = receipt.bundleIdData else { throw ReceiptValidationError.incorrectHash }
    guard let receiptHashData = receipt.sha1Hash else { throw ReceiptValidationError.incorrectHash }
    
    var deviceIdentifier = UIDevice.current.identifierForVendor?.uuid
    
    let rawDeviceIdentifierPointer = withUnsafePointer(to: &deviceIdentifier, {
        (unsafeDeviceIdentifierPointer: UnsafePointer<uuid_t?>) -> UnsafeRawPointer in
        return UnsafeRawPointer(unsafeDeviceIdentifierPointer)
    })
    
    let deviceIdentifierData = NSData(bytes: rawDeviceIdentifierPointer, length: 16)
    
    // Compute the hash for your app & device
    
    // Set up the hasing context
    var computedHash = Array<UInt8>(repeating: 0, count: 20)
    var sha1Context = SHA_CTX()
    
    SHA1_Init(&sha1Context)
    SHA1_Update(&sha1Context, deviceIdentifierData.bytes, deviceIdentifierData.length)
    SHA1_Update(&sha1Context, receiptOpaqueValueData.bytes, receiptOpaqueValueData.length)
    SHA1_Update(&sha1Context, receiptBundleIdData.bytes, receiptBundleIdData.length)
    SHA1_Final(&computedHash, &sha1Context)
    
    let computedHashData = NSData(bytes: &computedHash, length: 20)
    
    // Compare the computed hash with the receipt's hash
    guard computedHashData.isEqual(to: receiptHashData as Data) else { throw ReceiptValidationError.incorrectHash }
}

Walking through validateHash

First up, I’ve placed three guard statements:

1
2
3
guard let receiptOpaqueValueData = receipt.opaqueValue else { throw ReceiptValidationError.incorrectHash }
guard let receiptBundleIdData = receipt.bundleIdData else { throw ReceiptValidationError.incorrectHash }
guard let receiptHashData = receipt.sha1Hash else { throw ReceiptValidationError.incorrectHash }

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 NSData instance:

1
2
3
4
5
6
7
8
var deviceIdentifier = UIDevice.current.identifierForVendor?.uuid

let rawDeviceIdentifierPointer = withUnsafePointer(to: &deviceIdentifier, {
    (unsafeDeviceIdentifierPointer: UnsafePointer<uuid_t?>) -> UnsafeRawPointer in
    return UnsafeRawPointer(unsafeDeviceIdentifierPointer)
})

let deviceIdentifierData = NSData(bytes: rawDeviceIdentifierPointer, length: 16)

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 NSData.

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.

Final ReceiptValidator

Let’s put it all together into the final ReceiptValidator struct. Additions are highlighted below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct ReceiptValidator {
    let receiptLoader = ReceiptLoader()
    let receiptExtractor = ReceiptExtractor()
    let receiptSignatureValidator = ReceiptSignatureValidator()
    let receiptParser = ReceiptParser()
    
    func validateReceipt() -> ReceiptValidationResult {
        do {
            let receiptData = try receiptLoader.loadReceipt()
            let receiptContainer = try receiptExtractor.extractPKCS7Container(receiptData)
            
            try receiptSignatureValidator.checkSignaturePresence(receiptContainer)
            try receiptSignatureValidator.checkSignatureAuthenticity(receiptContainer)
            
            let parsedReceipt = try receiptParser.parse(receiptContainer)
            try validateHash(receipt: parsedReceipt)
            
            return .success(parsedReceipt)
        } catch {
            return .error(error as! ReceiptValidationError)
        }
    }
    
    fileprivate func validateHash(receipt: ParsedReceipt) throws {
        // Make sure that the ParsedReceipt instances has non-nil values needed for hash comparison
        guard let receiptOpaqueValueData = receipt.opaqueValue else { throw ReceiptValidationError.incorrectHash }
        guard let receiptBundleIdData = receipt.bundleIdData else { throw ReceiptValidationError.incorrectHash }
        guard let receiptHashData = receipt.sha1Hash else { throw ReceiptValidationError.incorrectHash }
        
        var deviceIdentifier = UIDevice.current.identifierForVendor?.uuid
        
        let rawDeviceIdentifierPointer = withUnsafePointer(to: &deviceIdentifier, {
            (unsafeDeviceIdentifierPointer: UnsafePointer<uuid_t?>) -> UnsafeRawPointer in
            return UnsafeRawPointer(unsafeDeviceIdentifierPointer)
        })
        
        let deviceIdentifierData = NSData(bytes: rawDeviceIdentifierPointer, length: 16)
        
        // Compute the hash for your app & device
        
        // Set up the hasing context
        var computedHash = Array<UInt8>(repeating: 0, count: 20)
        var sha1Context = SHA_CTX()
        
        SHA1_Init(&sha1Context)
        SHA1_Update(&sha1Context, deviceIdentifierData.bytes, deviceIdentifierData.length)
        SHA1_Update(&sha1Context, receiptOpaqueValueData.bytes, receiptOpaqueValueData.length)
        SHA1_Update(&sha1Context, receiptBundleIdData.bytes, receiptBundleIdData.length)
        SHA1_Final(&computedHash, &sha1Context)
        
        let computedHashData = NSData(bytes: &computedHash, length: 20)
        
        // Compare the computed hash with the receipt's hash
        guard computedHashData.isEqual(to: receiptHashData as Data) else { throw ReceiptValidationError.incorrectHash }
    }
}

What to do from here

So what now?

Well, at this stage, if no errors have been thrown, you’ve got a valid receipt.

The 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.

If a 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 ParsedReceipt because 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 ParsedReceipt to 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!

comments powered by Disqus