In-App Purchases: Receipt Validation Tutorial

In this tutorial, you’ll learn how receipts for In-App Purchases work and how to validate them to ensure your users have paid for the goodies you give them. By Bill Morefield.

Leave a rating/review
Download materials
Save for later
Share

Paid software has always presented a problem where some users try to use the software without buying it or to fraudulently access in-app purchases. Receipts provide a tool to confirm those purchases. They accomplish this by providing a record of sale. The App Store generates a receipt in the app bundle any time a user purchases your app, makes an in-app purchase or updates the app.

In this tutorial, you’ll learn how these receipts work and how they’re validated on a device. For this tutorial, you should be familiar with in-App Purchases and StoreKit. You will need an iOS developer account, a real device for testing, access to the iOS Developer Center and App Store Connect.

What Is a Receipt?

The receipt consists of a single file in the app bundle. The file is in a format called PKCS #7. This is a standard format for data with cryptography applied to it. The container contains a payload, a chain of certificates and a digital signature. You use the certificate chain and digital signature to validate that Apple produced the receipt.

The payload consists of a set of receipt attributes in a cross-platform format called ASN.1. Each of these attributes consists of a type, version and value. Together, these represent the contents of the receipt. Your app uses these attributes to both determine the receipt is valid for the device and what the user purchased.

Getting Started

Download the materials for this tutorial using the Download Materials button at either the top or bottom of this page. Inside, you’ll find a starter project. The starter project is an iPhone application that supports StoreKit and in-app purchases. See In-App Purchase Tutorial: Getting Started if you need a primer on StoreKit.

To test receipt validation, you must run the app on a real device, as it won’t work in the simulator. You’ll need a Development Certificate and a sandbox account. When testing an app through XCode, the app won’t have a receipt by default. The starter app implements requesting a refreshed certificate if one doesn’t exist.

Cryptographic code is complex and it’s easy to make mistakes. It’s better to use a known and validated library instead of trying to write your own. This tutorial uses OpenSSL libraries to do much of the work of verifying the cryptography and decoding the ASN.1 data provided in the receipt. OpenSSL isn’t very Swift-friendly, so you’ll be creating a Swift wrapper during this tutorial.

Compiling OpenSSL for the iPhone isn’t a simple process. You can find scripts and instructions on GitHub if you want to do it yourself. The starter project includes OpenSSL 1.1.1, the newest version, in the OpenSSL folder. It’s compiled as static libraries to make modification more difficult. This includes the folder as well as the C header files. The project also includes the bridge header to use the OpenSSL libraries from Swift.

Note: You may be wondering why you use OpenSSL instead of the CommonCrypto framework built into iOS, as the static OpenSSL libraries add about 40MB to your app bundle. The reason is if the user jailbreaks their device, it would be easy to replace CommonCrypto with a hacked version to work around these checks. A static library in the bundle is a more difficult target to attack.

Loading the Receipt

The starter project includes a starting Receipt class. It also contains a single static method: isReceiptPresent(). This method determines if a receipt file is present. If not, it uses StoreKit to request a refresh of the receipt before it attempts to validate it. Your app should do something similar if a receipt isn’t present.

Open Receipt.swift. Add a new custom initializer for the class at the end of the class declaration:

init() {
  guard let payload = loadReceipt() else {
    return
  }
}

To begin validation, you need the receipt as a Data object. Add the following new method to Receipt below init() to load the receipt and return the PKCS #7 data structure:

private func loadReceipt() -> UnsafeMutablePointer<PKCS7>? {
  // Load the receipt into a Data object
  guard 
    let receiptUrl = Bundle.main.appStoreReceiptURL,
    let receiptData = try? Data(contentsOf: receiptUrl) 
    else {
      receiptStatus = .noReceiptPresent
      return nil
  }
}

This code obtains the location of the receipt and attempts to load it as a Data object. If no receipt exists or the receipt won’t load as a Data object, then validation fails. If at any point during the validation of a receipt a check fails, then the validation as a whole has failed. The code stores the reason in the receiptStatus property of the class.

Now you have the receipt in a Data object, you can process the contents using OpenSSL. OpenSSL functions are written in C and generally work with pointers and other low level methods. Add the following code at the end of loadReceipt():

// 1
let receiptBIO = BIO_new(BIO_s_mem())
let receiptBytes: [UInt8] = .init(receiptData)
BIO_write(receiptBIO, receiptBytes, Int32(receiptData.count))
// 2
let receiptPKCS7 = d2i_PKCS7_bio(receiptBIO, nil)
BIO_free(receiptBIO)
// 3
guard receiptPKCS7 != nil else {
  receiptStatus = .unknownReceiptFormat
  return nil
}

How this code works:

  1. To work with the envelope in OpenSSL, you first must convert it into a BIO, which is an abstracted I/O structure used by OpenSSL. To create a new BIO object, OpenSSL needs a pointer to raw bytes of data in C. A C byte is a Swift UInt8. Since you can initialize an array from any Sequence and Data presents as a sequence of UInt8, you create the [UInt8] array by just passing in the Data instance. You then pass the array as the raw byte pointer. This is possible because Swift implicitly bridges function parameters, creating a pointer to an array’s elements. An OpenSSL call then writes the receipt into the BIO structure.
  2. You convert the BIO object into an OpenSSL PKCS7 data structure named receiptPKCS7. That done, you no longer need the BIO object and can free the memory you previously allocated for it.
  3. If anything goes wrong, then receiptPKCS7 will be a pointer to nothing or nil. In that case, set the status to reflect the validation failure.

Next, you need to ensure the container holds both a signature and data. Add the following code to the end of the loadReceipt() method to perform these checks:

// Check that the container has a signature
guard OBJ_obj2nid(receiptPKCS7!.pointee.type) == NID_pkcs7_signed else {
  receiptStatus = .invalidPKCS7Signature
  return nil
}

// Check that the container contains data
let receiptContents = receiptPKCS7!.pointee.d.sign.pointee.contents
guard OBJ_obj2nid(receiptContents?.pointee.type) == NID_pkcs7_data else {
  receiptStatus = .invalidPKCS7Type
  return nil
}

return receiptPKCS7

C normally handles complex data using a structure. Unlike Swift structures, C structures contain only data with no methods or other elements. References to a structure in C are references to the memory location — a pointer to the data structure.

Various UnsafePointer types exist to allow mixing Swift and C code. The OpenSSL function expects a pointer instead of the Swift classes and structures you’re likely more familiar with. receiptPKCS7 is a pointer to the data structure holding the PKCS #7 envelope. The pointee property of UnsafePointer follows the pointer to the data structure.

The process of referencing what a pointer points to in C is common enough to have a special operator ->. The pointee property of a pointer performs this reference in Swift.

If the checks succeed, then the method returns a pointer to the structure. Now that you have an envelope that’s in the correct format and contains data, you should verify that Apple signed it.