---
title: "Validating App Store Receipts without verifyReceipt"
description: "A fascinating look into how the App Store and StoreKit operate."
language: "en"
publishedAt: "2018-01-17T09:44:40Z"
updatedAt: "2018-01-17T09:44:40Z"
authors:
  - name: "Jacob Eiting"
    url: "https://www.revenuecat.com/blog/author/jacob-eiting"
category: "Engineering"
categoryUrl: "https://www.revenuecat.com/blog/engineering"
readingTime: 7
canonical: "https://www.revenuecat.com/blog/engineering/validating-app-store-receipts"
---

# Validating App Store Receipts without verifyReceipt

A fascinating look into how the App Store and StoreKit operate.

## Table of contents

- [App Store Receipts](#app-store-receipts)
- [Validating the Receipt Signature](#validating-the-receipt-signature)
- [Extracting the Receipt Fields](#extracting-the-receipt-fields)
- [Extracting In-App Purchases](#extracting-in-app-purchases)
- [Limitations](#limitations)

Apple receipts are not a black box. Since receipt files have been around, I’ve almost always treated them as tokens to be sent off to a service for validation, rather than actual data. I recently discovered that the App Store receipt’s contents are a full snapshot a user’s in-app-purchase (IAP) history and that Apple documents how to validate and extract this information. It is far from simple, but it provides a fascinating look into how the App Store and StoreKit operate.

## App Store Receipts

Starting with iOS 7, Apple introduced an alternative method for accessing a user’s IAP history. Previously developers had to rely on the `SKPaymentQueue` as the source of truth, listening to transactions and keeping track. With iOS 7, Apple introduced `NSBundle.appStoreReceiptURL` that gave developers a disk location they could find this new “receipt” file. The intention was to have developers use this file as the source of truth for IAPs, rather than the StoreKit purchase queue, which can be difficult to build around.

Most developers’ experience with App Store receipts is very brief. It entails grabbing the URL, reading the file’s contents, encoding it in base 64, and sending it off to a server for validation via Apple. The data itself is a black box to most, more of a token than an actual container of information. However, this couldn’t be further from the truth. The receipt is a complex but decently documented file that contains almost all of the information that you would normally get by validating via Apple’s `/verifyReceipt `endpoint.

## Validating the Receipt Signature

The file itself uses an old file encoding called ASN.1, a binary file encoding protocol similar to Protobuf or Apache Thrift. Using ASN.1, Apple wraps the actual receipt data (also an ASN.1 blob) inside of a cryptographic container known as a PKCS#7 container. It is a container that stores **signed data**, a **signature,** and **certificates** required to verify the included **signature**.

![](https://cdn.sanity.io/images/c3qnx9b0/production/09f659eed522327b6f0838ceef7adcbdcde4bd32-800x395.png)

*A diagram of the outermost format of the Apple Receipt, a container format commonly used for transporting signed data.*

We are using Python to decode and validate the receipt. First, we parse the PKCS#7 container and extract the essential parts: the **certificates**, the **signature**, and the **receipt data**.

```javascript
# Load the contents of the receipt file
receipt_file = open('.\/receipt_data.bin', 'rb').read()

# Use asn1crypto's cms definitions to parse the PKCS#7 format
from asn1crypto.cms import ContentInfo
pkcs_container = ContentInfo.load(receipt_file)

# Extract the certificates, signature, and receipt_data
certificates = pkcs_container['content']['certificates']
signer_info = pkcs_container['content']['signer_infos'][0]
receipt_data = pkcs_container['content']['encap_content_info']['content']
```

Apple relies on cryptographic signatures to guarantee that the contents of the receipt are valid. (It may be useful to brush up on asymmetric key cryptography if you aren’t familiar with the concept at a basic level.) Apple hashes the **receipt data**, then encrypts that hash with their private key. This ensures that the only way to decrypt the encrypted hash is using Apple’s published public key. This process is called **signing**, and the encrypted hash is called a **signature**.

‍

![](https://cdn.sanity.io/images/c3qnx9b0/production/cbd5ca605c2bf8947748690b27c5ca64d4a3c65a-800x395.png)

Apple hashes and encrypts the receipt data to create a signature, then packages the **signature**, **certificates**, and **receipt data** together into the **receipt file**.

Our job is to ensure the **receipt data** we have is the same data that Apple signed. Since Apple generated the **signature** with their private key (which only Apple can access), we can trust the data in the receipt file without talking to Apple directly. To do this, we use Apple’s public key, also known as a certificate. We first decrypt the signature to get back to the hash and then compute the hash ourselves using the included **receipt data** and the same hashing function. If our computed hash matches the one we’ve decrypted from Apple, we’ve proven that the **receipt data** contained in the **receipt file** is the same **receipt data** that Apple signed.

‍

![](https://cdn.sanity.io/images/c3qnx9b0/production/664b93bff72d2333a17c27b8d965ff6d115881ba-800x395.png)

*We use the enclosed certificates to recompute the decrypted hash from Apple. We need to validate the included certificates first though.*

For security reasons, Apple doesn’t like to use their root certificate to generate such simple signatures. If the private key for Apple’s root certificate were on every iTunes server, the chance of compromise would be much higher. Instead, Apple uses a certificate chain of trust to grant signing privileges to other, less critical certificates. Apple typically includes three certificates in the receipt file: the *Apple Root Certificate*, a *World Wide Developer Relations Intermediate Certificate*, and the *iTunes App Store Certificate*. The *iTunes App Store Certificate* is the one used to generate the **signature** of the **receipt data**.

```javascript
from OpenSSL.crypto import load_certificate, FILETYPE_ASN1

# Pull out and parse the X.509 certificates included in the receipt
itunes_cert_data = certificates[0].chosen.dump()
itunes_cert = load_certificate(FILETYPE_ASN1, itunes_cert_data)
itunes_cert_signature = certificates[0].chosen.signature

wwdr_cert_data = certificates[1].chosen.dump()
wwdr_cert = load_certificate(FILETYPE_ASN1, wwdr_cert_data)
wwdr_cert_signature = certificates[1].chosen.signature

untrusted_root_data = certificates[2].chosen.dump()
untrusted_root = load_certificate(FILETYPE_ASN1, untrusted_root_data)
untrusted_root_signature = certificates[2].chosen.signature
```

To bootstrap our trust chain, we need to obtain a trusted copy of the *Apple Root Certificate*. We can download a trusted *Apple Root Certificate* from Apple. Assuming your machine hasn’t been man-in-the-middled by some very tenacious IAP cracker, you can trust this certificate. Having a known trusted certificate, we can bootstrap trust to the other included **certificates** included in the receipt file.

```javascript
import urllib.request
trusted_root_data = urllib.request.urlopen("https:\/\/www.apple.com\/appleca\/AppleIncRootCertificate.cer").read()
trusted_root = load_certificate(FILETYPE_ASN1, trusted_root_data)
```

The trusted version of the *Apple Root Certificate* can be used to validate the signature on the *WWDR Certificate*. Once we’ve validated that the *WWDR Certificate* included in the receipt is trusted, we can, in turn, use it to validate the *iTunes App Store Certificate*. Fortunately, the OpenSSL Python wrapper provides tools for doing this.

```javascript
from OpenSSL.crypto import X509Store, X509StoreContext, X509StoreContextError
trusted_store = X509Store()
trusted_store.add_cert(trusted_root)

try:
  X509StoreContext(trusted_store, wwdr_cert).verify_certificate()
  trusted_store.add_cert(wwdr_cert)
except X509StoreContextError as e:
  print("WWDR certificate invalid")
  exit()

try:
  X509StoreContext(trusted_store, itunes_cert).verify_certificate()
except X509StoreContextError as e:
  print("iTunes certificate invalid")
  exit()
```

Now we have the *iTunes App Store Certificate* extracted and validated; we can use it to validate the **signature** on the receipt data.

```javascript
from OpenSSL.crypto import verify

try:
    verify(itunes_cert, signer_info['signature'].native, receipt_data.native, 'sha1')
    print("The receipt data signature is valid.")
except Exception as e:
    print("The receipt data is invalid: %s" % e)
```

We’ve now unpacked the PKCS#7 container, extracted the **certificates**, validated them using a trusted root cert, and used them to validate the **signature** on our **receipt data**. We can now trust that no one has modified the contents of the r**eceipt data** since Apple generated the **signature**. The next phase is unpacking the **receipt data** and reading the actual information about a user’s purchases.

## Extracting the Receipt Fields

The signed receipt data itself is an ASN.1 container. However, this one doesn’t use a public specification such as PKCS#7 or X.509. Instead, it is a custom format created by Apple exclusively for App Store receipts. You can see the [documented fields here](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html) (it’s likely you’ve used this page for reading Apple JSON responses). The following ASN.1 specification describes the contents of the **receipt data:**

```javascript
ReceiptModule DEFINITIONS ::=
BEGIN
 
ReceiptAttribute ::= SEQUENCE {
    type    INTEGER,
    version INTEGER,
    value   OCTET STRING
}
 
Payload ::= SET OF ReceiptAttribute
 
END
```

The definition is relatively loose, a record called a `ReceiptAttribute` that is a tuple of(Integer type, Integer version, Octet String value) and a set of these that we call Payload. The type field is used in conjunction with Apple’s documentation to tell you how to parse the value field, which is a binary blob. The asn1crypto module lets us define custom types to automate some of the parsing work.

```javascript
from asn1crypto.core import Any, Integer, ObjectIdentifier, OctetString, Sequence, SetOf, UTF8String, IA5String
attribute_types = [
    (2, 'bundle_id', UTF8String),
    (3, 'application_version', UTF8String) ,
    (4, 'opaque_value', OctetString),
    (5, 'sha1_hash', OctetString),
    (12, 'creation_date', IA5String),
    (17, 'in_app', OctetString),
    (19, 'original_application_version', UTF8String),
    (21, 'expiration_date', IA5String)
]

class ReceiptAttributeType(Integer):
    _map = {type_code: name for type_code, name, _ in attribute_types}

class ReceiptAttribute(Sequence):
    _fields = [
        ('type', ReceiptAttributeType),
        ('version', Integer),
        ('value', OctetString)
    ]

class Receipt(SetOf):
    _child_spec = ReceiptAttribute


receipt = Receipt.load(receipt_data.native)
```

We can now go over all the ReceiptAttribute tuples and move them into a dictionary since there should only be one of each type for all but one of the types. We also convert them to native Python types. There are, however, multiple instances of the in_app type. We will store these for later.

```javascript
receipt_attributes = {}
attribute_types_to_class = {name: type_class for _, name, type_class in attribute_types}

in_apps = []
for attr in receipt:
  attr_type = attr['type'].native

  # Just store the in_apps for now
  if attr_type == 'in_app':
    in_apps.append(attr['value'])
    continue

  if attr_type in attribute_types_to_class:
    if attribute_types_to_class[attr_type] is not None:
      receipt_attributes[attr_type] = attribute_types_to_class[attr_type].load(attr['value'].native).native
    else:
      receipt_attributes[attr_type] = attr['value'].native
```

The dictionary receipt_attributes now has some of the same data you are accustomed to seeing in response from Apple’s `/verifyReceipt` endpoint.

```javascript
The receipt data signature is valid.
{'application_version': '1',
 'bundle_id': 'com.jacobeiting.subtester',
 'creation_date': '2017-10-03T00:58:43Z',
 'opaque_value': b'\xdb\x81(\xb1=\xce.<i\xbdkS\xc6\xbe%\x08',
 'original_application_version': '1.0',
 'sha1_hash': b',\x88\x7f\x9f4\x1b\xc2\xa9W\x18\x1d\x81\x1c%\xbb\x91"*\xe97'}
in_apps count = 8
```

## Extracting In-App Purchases

The data we’ve extracted so far has to do with the app purchase or download, but we are usually more interested in the IAPs. To see what IAPs there are, we look at the in_app attributes that we siphoned off when going through the receipt’s attributes. The value field of in_app attributes is itself another ASN.1 container with a similar definition as the top-level receipt.

```javascript
InAppAttribute ::= SEQUENCE {
    type                   INTEGER,
    version                INTEGER,
    value                  OCTET STRING
}
 
InAppReceipt ::= SET OF InAppAttribute
```

We can perform the same operation on the contents of the individual in_app receipts to extract the fields as native Python objects.

```javascript
in_app_attribute_types = {
    (1701, 'quantity', Integer),
    (1702, 'product_id', UTF8String),
    (1703, 'transaction_id', UTF8String),
    (1705, 'original_transaction_id', UTF8String),
    (1704, 'purchase_date', IA5String),
    (1706, 'original_purchase_date', IA5String),
    (1708, 'expires_date', IA5String),
    (1719, 'is_in_intro_offer_period', Integer),
    (1712, 'cancellation_date', IA5String),
    (1711, 'web_order_line_item_id', Integer)
}

class InAppAttributeType(Integer):
    _map = {type_code: name for (type_code, name, _) in in_app_attribute_types}

class InAppAttribute(Sequence):
    _fields = [
        ('type', InAppAttributeType),
        ('version', Integer),
        ('value', OctetString)
    ]

class InAppPayload(SetOf):
    _child_spec = InAppAttribute

in_app_attribute_types_to_class = {name: type_class for _, name, type_class in in_app_attribute_types}

in_apps_parsed = []

for in_app_data in in_apps:
  in_app = {}

  for attr in InAppPayload.load(in_app_data.native):
    attr_type = attr['type'].native

    if attr_type in in_app_attribute_types_to_class:
      in_app[attr_type] = in_app_attribute_types_to_class[attr_type].load(attr['value'].native).native

  in_apps_parsed.append(in_app)

receipt_attributes['in_app'] = in_apps_parsed

from pprint import pprint as pp
pp(receipt_attributes)
```

And that’s the way the news goes! We’ve successfully unpacked and validated the information from an Apple receipt without sending it off to Apple.

```javascript
{'application_version': '1',
 'bundle_id': 'com.jacobeiting.subtester',
 'creation_date': '2017-10-03T00:58:43Z',
 'in_app': [{'cancellation_date': '',
             'expires_date': '2017-10-02T23:16:23Z',
             'original_purchase_date': '2017-10-02T23:08:25Z',
             'original_transaction_id': '1000000339835086',
             'product_id': 'onemonth_freetrial',
             'purchase_date': '2017-10-02T23:11:23Z',
             'quantity': 1,
             'transaction_id': '1000000339835183',
             'web_order_line_item_id': 1000000036447552},
            {'cancellation_date': '',
             'expires_date': '2017-10-02T23:21:23Z',
             'original_purchase_date': '2017-10-02T23:08:25Z',
             'original_transaction_id': '1000000339835086',
             'product_id': 'onemonth_freetrial',
             'purchase_date': '2017-10-02T23:16:23Z',
             'quantity': 1,
             'transaction_id': '1000000339835291',
             'web_order_line_item_id': 1000000036447572},
            {'cancellation_date': '',
             'expires_date': '2017-10-02T23:11:23Z',
             'original_purchase_date': '2017-10-02T23:08:25Z',
             'original_transaction_id': '1000000339835086',
             'product_id': 'onemonth_freetrial',
             'purchase_date': '2017-10-02T23:08:23Z',
             'quantity': 1,
             'transaction_id': '1000000339835086',
             'web_order_line_item_id': 1000000036447550}],
 'opaque_value': b'\xdb\x81(\xb1=\xce.<i\xbdkS\xc6\xbe%\x08',
 'original_application_version': '1.0',
 'sha1_hash': b',\x88\x7f\x9f4\x1b\xc2\xa9W\x18\x1d\x81\x1c%\xbb\x91"*\xe97'}
```

[Here is the script in its entirety.](https://gist.github.com/jeiting/84b724b2d9438cbb41227e144e5c3615)

## Limitations

There are limitations to validating Apple receipts this way. The biggest being that you won’t get access to fields associated with subscriptions: latest_receipt_info and pending_renewal_info. The receipt file you unpack is just a snapshot, and you potentially need to be able to update a user’s subscriptions even if they haven’t sent you the most recent receipt. Additionally, validating receipts yourself is error-prone and could be equated with “rolling your own crypto” – risky and usually unneeded. For instance, **DO NOT USE THIS CODE** to validate your receipts.

We skipped over some important things like checking certificate expirations. You are better off using Apple’s `/verifyReceipt` service or something like RevenueCat if you don’t want to handle the risk. [RevenueCat](https://www.revenuecat.com) is the best way to implement subscriptions in your mobile app. We handle all the complicated parts so you can get back to building.

‍
