Introducing ReceiptParser for Apple Platforms

A new open-source Swift-only package for parsing StoreKit receipts locally.

Introducing ReceiptParser for Apple Platforms
Nacho Soto

Nacho Soto

PublishedLast updated

Receipts contain a full snapshot of the in-app purchase (IAP) history for the user. Apps typically send them to Apple’s /verifyReceipt endpoint for verification, which also allows extracting their contents. However, this is not supposed to be done directly from the device, as doing so would be susceptible to a MITM attack. We’ve covered in the past how to validate receipts without verifyReceipt.

Since that blog post, Apple has launched StoreKit 2, which introduced new APIs to extract information from the device, using AppTransaction or Transaction.

However, for apps not using StoreKit 2 yet, or in order to support iOS versions prior to 15.0, Foundation has exposed Bundle.appStoreReceiptURL for many years now. The content of this receipt is not a blackbox and can be introspected.

This receipt contains binary data encoded using the ASN.1 (Abstract Syntax Notation One) standard. Apple has comprehensive documentation on the contents of these receipts, but it’s not a trivial task to parse it (more details on this below).

ReceiptParser

That’s where ReceiptParser comes in. This has been part of the RevenueCat SDK for a while now, to optimize the process of sending receipts to our backend, free trial eligibility computation, and for several debugging features.

Starting from version 4.16 of our iOS SDK, it is now a public API available to be used by anyone.

The parser is also available from the main RevenueCat SDK in case you’re already using that, but we’ve also created a new small ReceiptParser Swift Package (SPM) that you can use directly without needing to depend on the entire RevenueCat framework.

To use it, select File -> Add Packages… in Xcode, then type github.com/RevenueCat/purchases-ios. You’ll be prompted to select either RevenueCat or the new ReceiptParser.

Using the ReceiptParser Swift Package in Xcode
Figure 1. The ReceiptParser can be used directly within Xcode with the ReceiptParser Swift Package (SPM).

Parsing a receipt is as simple as this:

1let parser = PurchasesReceiptParser.default
2let receipt: AppleReceipt = try parser.parse(from: receiptData)

You can also load the local receipt directly:

1let receipt: AppleReceipt = try parser.fetchAndParseLocalReceipt()

From there, you can use the extensive AppleReceipt API to extract all the necessary information.

For example, you could have a function that checks if there are any active subscriptions:

1extension AppleReceipt.InAppPurchase {
2    var isSubscription: Bool {
3        switch self.productType {
4        case .unknown: return self.expiresDate != nil
5        case .nonConsumable, .consumable: return false
6        case .nonRenewingSubscription, .autoRenewableSubscription: return true
7        }
8    }
9
10  var isActiveSubscription: Bool {
11        guard self.isSubscription, let expiration = self.expiresDate else { return false }
12
13        return expiration > Date()
14    }
15}
16
17extension AppleReceipt {
18  var containsActiveSubscription: Bool {
19    return self.inAppPurchases.contains { $0.isActiveSubscription }  
20  }
21}

We’ve also added a receipt visualization to PurchaseTester, our testing app for the SDK:

We have added receipt visualization to PurchaseTester, our testing app for the SDK.
Figure 2. Receipt visualization in PurchaseTester, our testing app for the SDK.

Note that this is meant for debugging purposes only. If you would like to verify the purchases a user has made, either perform receipt validation on your own or use RevenueCat’s SDK, which performs validation in our backend. The contents of this receipt should not be trusted however since there is no on-device validation.

How does ReceiptParser work?

The actual receipt file is Data, i.e. a binary blob.

It’s interpreted as an ASN.1 container — a format that gives you information about its contents’ size and type, and that can hold other ASN.1 containers inside recursively.

Each ASN.1 container is a Tag-Length-Value (TLV) triplet:

Type: identifier + class + encoding type.

  • The identifier gives you general info about the type of data found in value, i.e. string, bool, set, etc.
  • The encoding type (primitive | constructed) tells you whether the container’s payload contains another ASN.1 container inside.

Length: holds information about the container’s size. It can be:

  • Single byte – if the length < 128. This is indicated by the first bit == 0. The value of the next 7 bits indicates the length of the value of the container.
  • Multi-byte – if the length > =128. This is indicated by having the first length bit == 1. The value of the next 7 bits indicates the number of bytes used to for the length (let’s call this N). The next N bytes hold the actual value of the length of the value of the container.

Value:

  • Can be any number of things — from an ASN.1 container, to a list of containers, to a primitive value.

Everything in the receipt is an ASN.1 container, and most components recursively hold multiple levels of containers.

ASN.1 container:

ASN1 container
Figure 3. ASN.1 container

Type (first byte in the container):

Figure 4. The first byte in the container is a Type, giving you a class, encoding type, and identifier.
Figure 4. The first byte in the container is a Type, giving you a class, encoding type, and identifier.

Length (next N bytes in the container, depending on how long it is):

Figure 5. Length holds information about a container's size, which can either be single byte or multi byte.
Figure 5. Length holds information about a container’s size, which can either be single byte or multi byte.

Finding the receipt payload

The actual payload of the receipt is found by digging into the containers and finding one with the ASN.1 object identifier “1.2.840.113549.1.7.1”.

Object identifiers are globally unique, and this one corresponds to “data”, as registered here.

The container immediately after the object identifier for “data” is the receipt payload.

Other object identifiers are used for things like the signature.

Decoding the object identifier

The object identifier is encoded as specified in Microsoft’s documentation.

Essentially, it’s a string of numbers, separated by “.”. Each of those numbers between the dots is a “node”.

The first two nodes are encoded into the first byte, with the first node multiplied by 40 and the result added to the second node. This means decoding those is:

  • First node: first byte / 40.
  • Second node: first byte % 40.

The rest of the nodes follow an entirely different encoding, known as Variable-length quantity.

This format is fairly simple and designed to describe an integer with as few bytes as possible.

The idea is that you grab the original bytes and then split it up into groups of 7 bits.

Let’s say you’re encoding the value 11645 and that you’re using 3 bytes to encode numbers.

This means your original number is 00000000 00101101 01111101.

To encode in Variable-length quantity (VLQ):

  • Split everything into groups of 7: you get 000 0000000 1011010 1111101.
  • Kill groups with all zeroes to the left: 1011010 1111101.
  • In each group, add an extra digit: it will be 1 for all groups except the last. This digit is used when decoding, to indicate whether the number keeps going on the next byte.
  • This gives you the final number, 11011010 01111101.

To decode in VLQ, then, we iterate through the bytes:

  • If the first bit is 1, this indicates a long-format number.
  • if the first bit is 0, this indicates either a short-format number or the end of a long-format number.
  • While the first bit is 1, grab the 7 right-most bits of the byte, and append them to a buffer.
  • Once you find a 0 on the first bit of a byte, close the buffer, and interpret the contents of the buffer as a binary.

Each time a number is closed, you have a node. Keep going until all numbers in the container are read.

When all nodes are decoded, just create a string that joins them using “.”.

Interpreting the receipt payload

Once you do have the ASN.1 container that holds the actual payload, all that’s left is interpreting the contents.

From “Validating receipts on the device“, the receipt format definition:

Figure 6. ASN-1 definition of the payload format.
Figure 6. ASN.1 definition of the payload format.

What this means is that in the ASN.1 payload, there’ll be one Set (i.e.: unordered sequence) of ASN.1 containers that represent ReceiptAttributes.

Each of those ASN.1 containers holds three internal containers: one for the type, one for version, one for the value.

The type may be defined here, under ASN1.Field Type. If the type is not there that means it’s meant for Apple’s eyes only.

Although the value is an octet string, it may actually be an ASN.1 container (in fact, it is for all types except for opaque value and sha1 hash from our testing).

For example, if the type is 17 (IAP), it’s meant to be interpreted with the following definition (pretty much the same as the receipt):

Figure 7. ASN.1 definition of type 17 container.

Primitive types can just be decoded from the binary payload of the attribute container, i.e. strings are UTF-8 encoded, dates are RFC3306 ascii-encoded strings, etc.

In summary

Hopefully this summary sheds some light into the underlying format in which IAP receipts are encoded.

We can’t wait to see what debugging tools you build using this new framework!

In-App Subscriptions Made Easy

See why thousands of the world's tops apps use RevenueCat to power in-app purchases, analyze subscription data, and grow revenue on iOS, Android, and the web.

Related posts

Use cases for RevenueCat Billing
Engineering

Use cases for RevenueCat Billing

3 ways you can use the new RevenueCat Billing beta today.

Charlie Chapman

Charlie Chapman

March 21, 2024

Vision Pro apps powered by RevenueCat: What’s available on launch day
Engineering

Vision Pro apps powered by RevenueCat: What’s available on launch day

Over 160 of the first visionOS apps are powered by RevenueCat

Charlie Chapman

Charlie Chapman

February 2, 2024

How I successfully migrated my indie app to RevenueCat Paywalls
Engineering

How I successfully migrated my indie app to RevenueCat Paywalls

And how a simple experiment increased my LTV by 33%.

Charlie Chapman

Charlie Chapman

January 3, 2024

Want to see how RevenueCat can help?

RevenueCat enables us to have one single source of truth for subscriptions and revenue data.

Olivier Lemarié, PhotoroomOlivier Lemarié, Photoroom
Read Case Study