In-App Purchases in iOS. Part 4: Receipt validation

Sergey Zhuravel
11 min readNov 22, 2022

--

I am continuing a series of articles about IAP. In my previous article, I talked about how to test purchases through Sandbox, TestFlight, and locally in Xcode. In this article, you’ll learn how receipts work and how they’re validated on a device or your server. For this article, you should be familiar with IAP and StoreKit. You will need an iOS developer account, a device for testing, and access to the iOS Developer Center and AppStore Connect.

Links to all my articles about IAP:

  1. In-App Purchases in iOS. Part 1: Creating purchases and adding to the project.
  2. In-App Purchases in iOS. Part 2: Initialization and processing of purchases.
  3. In-App Purchases in iOS. Part 3: Testing purchases in TestFlight, Sandbox and locally in Xcode.
  4. In-App Purchases in iOS. Part 4: Receipt validation.

An App Store receipt provides a record of the sale of an app and any purchases the person makes within the app. You can authenticate purchased content by adding a receipt validation code to your app or server. Receipt validation requires an understanding of secure coding techniques to employ a safe and unique solution to your app.

Choose a validation technique

There are two ways to verify a receipt’s authenticity:

  • Locally, on the device. Validating receipts locally requires code that reads and validates a binary file that Apple encodes and signs as a PKCS #7 container.
  • On your server with the App Store. Validating receipts with the App Store requires secure connections between your app and your server, and between your server and the App Store.

Compare the approaches and determine the method that best fits your app and your infrastructure. You can also choose to implement both approaches. For managing auto-renewable subscriptions, see the following table for the key advantages that server-side receipt validation provides over on-device receipt validation:

Comparison of validation methods

Receipts contain non-consumable IAPs, auto-renewable subscriptions, and non-renewing subscriptions indefinitely. Consumable IAPs remain in the receipt until you call finishTransaction(_:). You may choose to maintain and manage records of consumable IAPs on your server.

In-App Purchase Process

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. Let’s have a look at the receipt structure:

Receipt structure

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 whether the receipt is valid for the device and what the user purchased.

Local Receipt Validation

The receipt issued to an app by the App Store contains a complete record of a user’s IAP history for that app. It is a signed and encrypted file stored on the device in the app’s main bundle. The location of the receipt is given by the URL Bundle.main.appStoreReceiptURL.

When an app is first installed the receipt will be missing. A new receipt will be issued automatically by the App Store when:

  • An IAP succeeds
  • The app is updated (a receipt is issued for the new version)
  • Previous IAPs are restored

The containing structure for the receipt is a PKCS #7 struct. PKCS #7 is a common cryptographic data format that OpenSSL handles for us.

The Payload part of the receipt contains zero or more Attributes in ASN.1 format (another common crypto format that OpenSSL works with). Each attribute is a record of an IAP.

I use the Certificate Chain and Signature to validate that the receipt was genuinely issued by Apple.

When should you validate the Receipt?

You should validate the app’s receipt:

  • On start-up
    Your app should keep a “fallback” list of successfully purchased product ids that are stored either in UserDefaults (easy to work with, less secure) or the Keychain (less easy to work with, more secure). This list will be useful if the receipt is missing and there’s no network connection allowing a fresh one to be requested from the App Store. At the start-up validate the receipt and then compare the fallback list against the IAP records in the receipt. If they differ, reset the fallback list to match the receipt.
  • When a purchase succeeds
    A new receipt will be issued automatically by the App Store when a purchase is successfully completed. The new receipt will be available in the app bundle when paymentQueue(_:updatedTransactions:) is called by StoreKit.
  • When purchases are restored
    This appears in the app as a succession of purchases. You should validate the receipt when the final transaction is completed (see paymentQueue(_:updatedTransactions:))

IAPManager demo example

I added a local validation implementation to my IAPManager project: it provides a more complete, real-world treatment of handling IAPs.

The main things to note are:

  • On-device receipt validation is supported using OpenSSL.
  • Purchased product ids are persisted to UserDefaults as a "fallback" list, and then checked against IAP data in the receipt.

The IAPManager.processReceipt() the method is used to validate App Store receipts. If you review this method, you'll see the main validation flow:

OpenSSL

IAPManager uses OpenSSL to validate the App Store receipt and read its contents. Building OpenSSL for iOS is not totally straightforward. To make getting started easier, the IAPManager project contains pre-built OpenSSL binaries that were built using version 1.1.1 of OpenSSL.

The OpenSSL binaries libcrypto.a and libssl.a need to work in the following environments:

As you can see from the above table, everything works as anticipated, except in the case of building with Xcode on an M1 Mac for running on the simulator. At the time of writing (available in November 2022), the situation isn’t totally clear. If we intend only to support recent devices on iOS 13 and further, in theory, our OpenSSL binaries only need to support two architectures in a “fat” or Universal Binary: x86 64-bit and ARM 64-bit.

The included builds of the OpenSSL binaries contain the following architectures as shown by using the lipo utility:

% lipo -info libcrypto.a 
Architectures in the fat file: libcrypto.a are: armv7 armv7s x86_64 arm64

% lipo -info libssl.a
Architectures in the fat file: libssl.a are: armv7 armv7s x86_64 arm64

Our IAPManager app only supports devices running iOS 13 and further. So IAPManager supports the iPhone 6s and upwards.

The arm64 64-bit ARM CPU architecture has been used since the iPhone 5S and iPad Air, Air 2, and Pro, with the A7 and later chips. The armv7s 32-bit architecture is used in Apple's A6 and A6X chips on iPhone 5, iPhone 5C, and iPad 4. The armv7 32-bit architecture is an older variant of the 32-bit ARM CPU.

If we build on an M1 Mac for the simulator we get the following error:

libcrypto.a(tasn_typ.o), building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Currently, I can’t find a solution to this issue. I wonder if it’s because the OpenSSL binaries were built for iOS arm64, which is in some way different for the arm64 architecture that the simulator running on the M1 Mac expects.

Note: If you build on an M1 Mac for a real device, then everything builds, links, and runs as expected. The issue is purely with the simulator.

It is possible to build for the simulator on an M1 Mac if you exclude the arm64 architecture for simulator builds:

However, when you run the app on the simulator you may face issues. The most notable one is that the Bundle.main.appStoreReceiptURL property, which points to the location of the App Store receipt, is always nil.

Until you come up with a solution, you need to build and deploy IAPManager to a real device if you use an M1-based Mac.

Loading the Receipt

The IAPReceipt the class encapsulates the main features and data of the App Store receipt. This includes a Set<ProductId> that holds a collection of purchased product ids that have been validated against data in the App Store receipt.

The load() method of the IAPReceipt class loads the receipt and performs basic validation:

Load the receipt data from the main bundle and cache it. Basic validation of the receipt is done. We check its format, if it has a signature and if contains data. After loading the receipt you should call validateSigning() to check the receipt has been correctly signed, then read its IAP data using read(). You can thenvalidate()the receipt. Returns: Returns true if loaded correctly, false otherwise.

Reading Receipt records

The read() method of the IAPReceipt the class reads the receipt's IAP data and caches it:

Read internal receipt data into a cache. Returns: Returns true if all expected data was present and correctly read from the receipt, false otherwise.

Validating the Receipt

The validate() method of the IAPReceipt the class performs the actual receipt validation:

Perform on-device (no network connection required) validation of the app’s receipt. Returns false if the receipt is invalid or missing. In this case your app should call refreshReceipt(completion:) to request an updated receipt from the app store. This may result in the user being prompted for their App Store credentials.

We validate the receipt to ensure that it was:

  • Created and signed using the Apple x509 root certificate via the App Store.
  • Issued for the same version of this app and the user’s device.

At this point, a list of locally stored purchased product ids should have been loaded from the UserDefaults dictionary. We need to validate these product ids against the App Store receipt’s collection of purchased product ids to see that they match. If there are no locally stored purchased product ids (i.e. the user hasn’t purchased anything), we don’t attempt to validate the receipt. Note that if the user has previously purchased products, either using the Restore feature or attempting to re-purchase the product will result in a refreshed receipt and the product id of the product will be stored locally in the UserDefaults dictionary. Returns: Returns true if the receipt is valid, false otherwise.

Validating receipts with the App Store

An App Store receipt is a binary encrypted file signed with an Apple certificate. In order to read the contents of the encrypted file, you need to pass it through the verifyReceipt endpoint. The endpoint’s response includes a readable JSON body. Communication with the App Store is structured as JSON dictionaries, as defined in RFC 4627. Binary data is Base64-encoded, as defined in RFC 4648. Validate receipts with the App Store through a secure server.

Warning: Don’t call the App Store server verifyReceipt endpoint from your app. You can’t build a trusted connection between a user’s device and the App Store directly, because you don’t control either end of that connection, which makes it susceptible to a machine-in-the-middle attack.

This warning is in the Apple documentation, so I recommend to follow it strictly. In my example, I will show the implementation without a server, but in production, you need to use validation only through your server.

Loading the Receipt data

The app receipt is always present in the production environment on devices running macOS, iOS, and iPadOS. In the sandbox environment and in StoreKit Testing in Xcode, the app receipt is present only after the tester makes the first IAP. If the app calls SKReceiptRefreshRequest or restoreCompletedTransactions(), the app receipt is present only if the app has at least one IAP.

To retrieve the receipt data from the app on the device, use the appStoreReceiptURL method of NSBundle to locate the app’s receipt, and encode the data in Base64. Send this Base64-encoded data to your server.

Here’s an example:

Send the receipt data to the App Store

To send a request to the server, you need the Shared Secret. This is a special key string that is required to decrypt checks with auto-renewable purchases. Apple uses the Shared secret as a parameter in the HTTPS request to Apple.

To generate it, you need to go to App Store Connect:

  1. From My Apps, select your app.
  2. In the sidebar under Features, click Subscriptions.
  3. In the App-Specific Shared Secret section, click on the Manage button:

4. In the pop-up that appears, click on the Generate button:

The next step is to send a POST request to the Apple server. In receiptAppStoreValidation()method, I send a receipt for validation, in JSON I pass the receipt itself and the Shared Secret.

Don’t forget to replace YOUR_SHARED_SECRET with your shared secret. I created a validationURLString variable into which I set the server URL depending on the TestFlight, Sandbox, or production user.

Best Practice: Apple recommends first sending a receipt to Production. If the receipt is for Sandbox, the response will contain a status field with the value 21007. This is your signal to try the Sandbox endpoint instead.

I created ReceiptStatus enum for easy parsing of validation status. If the request was successful, you will receive an HTTP 200 OK response code. This means we can expect to receive a JSON responseBody. The first thing we need to inspect from the responseBody is the status field. If the status is 0, the receipt is valid and many other fields will be present in the responseBody as well.

For now, we will use the status value to print a message explaining whether the receipt was validated or not. Just in case we don’t receive that HTTP 200 OK we were expecting, we’ll also catch and print any unexpected HTTP response codes here as well. In production, you can expect to see non-200 responses from Apple so you will need to add logic to handle this and retry if need be.

Here are the most common status values you will encounter:

  • 0 — The receipt is valid
  • 21002 — The encoded receipt passed into the requestBody’s receipt-data property is malformed
  • 21004 — The shared secret provided in the requestBody’s password property does not match what is on file with Apple
  • 21007 — The receipt is from the Sandbox environment, but it was sent to the Production verifyReceipt endpoint
  • 21008 — The receipt is from the Production environment, but it was sent to the Sandbox verifyReceipt endpoint

Parse the response

The App Store’s response payload is a JSON object that contains the keys and values detailed in responseBody.

The in_app the array contains the non-consumable, non-renewing subscription, and auto-renewable subscription items previously purchased by the user. Check the values in the response for these IAP types to verify transactions as needed.

For auto-renewable subscription items, parse the response to get information about the currently active subscription period. When you validate the receipt for a subscription, latest_receipt contains the latest encoded receipt, which is the same as the value for receipt-data in the request, and latest_receipt_info contains all the transactions for the subscription, including the initial purchase and subsequent renewals but not including any restores.

You can use these values to check whether an auto-renewable subscription has expired. Use these values along with the expiration_intent subscription field to get the reason for expiration.

Contact me: My Twitter
Full source code: GitHub

--

--

Sergey Zhuravel

iOS Engineer @ Futurra Group | Reverse-Engineering iOS Apps