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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
In-App Purchases: Receipt Validation Tutorial
30 mins
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.
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:
- 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 SwiftUInt8
. Since you can initialize an array from anySequence
andData
presents as a sequence ofUInt8
, you create the[UInt8]
array by just passing in theData
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 theBIO
structure. - 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. - If anything goes wrong, then
receiptPKCS7
will be a pointer to nothing ornil
. 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.
Validating Apple Signed the Receipt
A PKCS #7 container uses public key encryption with two components. One component is the public key shared with everyone. The second is a private secure key. Apple can digitally sign data with the private key so anyone with the corresponding public key can ensure that someone with the private key did the signing.
For the receipt, Apple uses its private key to sign the receipt, and you verify it using Apple’s public key. Certificates contain the information about these keys.
It’s common to use a certificate to sign other certificates that form a certificate chain. Doing so reduces the risk of compromising any one certificate as it only affects certificates lower in the chain. This allows a single root certificate at the top of the chain to verify the signature and intermediate certificates without being signed directly by the root certificate.
OpenSSL can deal with this check for you. Add the following call at the end of init()
:
guard validateSigning(payload) else {
return
}
Now add a new method to perform the check at the end of Receipt
:
private func validateSigning(_ receipt: UnsafeMutablePointer<PKCS7>?) -> Bool {
guard
let rootCertUrl = Bundle.main
.url(forResource: "AppleIncRootCertificate", withExtension: "cer"),
let rootCertData = try? Data(contentsOf: rootCertUrl)
else {
receiptStatus = .invalidAppleRootCertificate
return false
}
let rootCertBio = BIO_new(BIO_s_mem())
let rootCertBytes: [UInt8] = .init(rootCertData)
BIO_write(rootCertBio, rootCertBytes, Int32(rootCertData.count))
let rootCertX509 = d2i_X509_bio(rootCertBio, nil)
BIO_free(rootCertBio)
}
This code loads Apple’s root certificate from the bundle and converts it to a BIO object. Note a different function call reflects you’re loading an X.509 format certificate instead of a PKCS container. Add the following code to finish validateSigning(_:)
:
// 1
let store = X509_STORE_new()
X509_STORE_add_cert(store, rootCertX509)
// 2
OPENSSL_init_crypto(UInt64(OPENSSL_INIT_ADD_ALL_DIGESTS), nil)
// 3
let verificationResult = PKCS7_verify(receipt, nil, store, nil, nil, 0)
guard verificationResult == 1 else {
receiptStatus = .failedAppleSignature
return false
}
return true
How this code works:
- Use OpenSSL to create an X.509 certificate store. This store is a container of certificates for verification. The code adds the loaded root certificate to the store.
- Initialize OpenSSL for certificate validation.
- Use
PKCS7_verify(_:_:_:_:_:_:)
to verify a certificate in the chain from the root certificate signed the receipt. If so, the function returns1
. Any other value indicates the envelope wasn’t signed by Apple so validation fails.
Reading Data in the Receipt
Having verified Apple signed the receipt, you can now read the receipt contents. As described earlier, the contents of the payload is a set of ASN.1 values. You’ll use OpenSSL functions that read this format.
Receipt
already contains properties to store the payload contents. Add the following code at the end of init()
:
readReceipt(payload)
Add the following method after loadReceipt()
to start reading the receipt data:
private func readReceipt(_ receiptPKCS7: UnsafeMutablePointer<PKCS7>?) {
// Get a pointer to the start and end of the ASN.1 payload
let receiptSign = receiptPKCS7?.pointee.d.sign
let octets = receiptSign?.pointee.contents.pointee.d.data
var ptr = UnsafePointer(octets?.pointee.data)
let end = ptr!.advanced(by: Int(octets!.pointee.length))
}
This code gets a pointer to the start of the payload — as ptr
— from the PKCS7 structure. You then place a pointer to the end of the payload in end
. Add the following code to readReceipt(_:)
to start parsing the payload:
var type: Int32 = 0
var xclass: Int32 = 0
var length: Int = 0
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_SET else {
receiptStatus = .unexpectedASN1Type
return
}
There are three variables to store information about each ASN.1 object. ASN1_get_object(_:_:_:_:_:)
reads the buffer to get the first object. The pointer updates to the next object.
C functions often return multiple values from a function using pointers to variables and updating those objects directly. This is similar to an inout
parameter in Swift. The &
symbol gets the pointer to an object. The function returns the length of the data (length
), the ASN.1 object type (type
), and the ASN.1 tag value (xclass
).
The final parameter is the longest length to read. Providing this prevents a security issue caused by reading past the end of a memory area.
You then verify that the type of the first item in the payload is an ASN.1 set. If not, the payload isn’t valid. Otherwise, you can start reading the contents of the set. You will use similar calls to ASN1_get_object(_:_:_:_:_:)
to read all data in the payload. ASN1Helpers.swift contains several helper methods that read the ASN.1 data types found in a receipt into nullable Swift values. Add this code at the end of readReceipt(_:)
:
// 1
while ptr! < end {
// 2
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_SEQUENCE else {
receiptStatus = .unexpectedASN1Type
return
}
// 3
guard let attributeType = readASN1Integer(ptr: &ptr, maxLength: length) else {
receiptStatus = .unexpectedASN1Type
return
}
// 4
guard let _ = readASN1Integer(ptr: &ptr, maxLength: ptr!.distance(to: end)) else {
receiptStatus = .unexpectedASN1Type
return
}
// 5
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_OCTET_STRING else {
receiptStatus = .unexpectedASN1Type
return
}
// Insert attribute reading code
}
What this code does:
- Create a loop that runs until the pointer reaches the end of the payload. At that point you've processed the entire payload.
- Check that the object is a sequence. Each attribute is a sequence of three fields: type, version, data.
- Fetch the attribute type — an integer — that you'll use shortly.
- Read the attribute version, an integer. You won't need it for receipt validation.
- Check that the next value is a sequence of bytes.
As before, if any values are not as expected, you set a status code and the validation fails.
You now have information about the current attribute. You also have the type of data and the pointer to the data for this attribute. Apple documents the attributes in a receipt.
You'll use a switch statement to process the types of attributes found in a receipt. Replace the // Insert attribute reading code here
comment with the following:
switch attributeType {
case 2: // The bundle identifier
var stringStartPtr = ptr
bundleIdString = readASN1String(ptr: &stringStartPtr, maxLength: length)
bundleIdData = readASN1Data(ptr: ptr!, length: length)
case 3: // Bundle version
var stringStartPtr = ptr
bundleVersionString = readASN1String(ptr: &stringStartPtr, maxLength: length)
case 4: // Opaque value
let dataStartPtr = ptr!
opaqueData = readASN1Data(ptr: dataStartPtr, length: length)
case 5: // Computed GUID (SHA-1 Hash)
let dataStartPtr = ptr!
hashData = readASN1Data(ptr: dataStartPtr, length: length)
case 12: // Receipt Creation Date
var dateStartPtr = ptr
receiptCreationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
case 17: // IAP Receipt
print("IAP Receipt.")
case 19: // Original App Version
var stringStartPtr = ptr
originalAppVersion = readASN1String(ptr: &stringStartPtr, maxLength: length)
case 21: // Expiration Date
var dateStartPtr = ptr
expirationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
default: // Ignore other attributes in receipt
print("Not processing attribute type: \(attributeType)")
}
// Advance pointer to the next item
ptr = ptr!.advanced(by: length)
This code uses the type of each attribute to call the appropriate helper function, which will put the value into a property of the class. After reading each value, the last line advances the pointer to the start of the next attribute before continuing the loop.
Reading In-App Purchases
The attribute for in-app purchases requires more complex processing. Instead of a single integer or string, in-app purchases are another ASN.1 set within this set. IAPReceipt.swift contains an IAPReceipt
to store the contents. The set is formatted the same as the one containing it and the code to read it is very similar. Add the following initializer to IAPReceipt
:
init?(with pointer: inout UnsafePointer<UInt8>?, payloadLength: Int) {
let endPointer = pointer!.advanced(by: payloadLength)
var type: Int32 = 0
var xclass: Int32 = 0
var length = 0
ASN1_get_object(&pointer, &length, &type, &xclass, payloadLength)
guard type == V_ASN1_SET else {
return nil
}
while pointer! < endPointer {
ASN1_get_object(&pointer, &length, &type, &xclass, pointer!.distance(to: endPointer))
guard type == V_ASN1_SEQUENCE else {
return nil
}
guard let attributeType = readASN1Integer(ptr: &pointer,
maxLength: pointer!.distance(to: endPointer))
else {
return nil
}
// Attribute version must be an integer, but not using the value
guard let _ = readASN1Integer(ptr: &pointer,
maxLength: pointer!.distance(to: endPointer))
else {
return nil
}
ASN1_get_object(&pointer, &length, &type, &xclass, pointer!.distance(to: endPointer))
guard type == V_ASN1_OCTET_STRING else {
return nil
}
switch attributeType {
case 1701:
var p = pointer
quantity = readASN1Integer(ptr: &p, maxLength: length)
case 1702:
var p = pointer
productIdentifier = readASN1String(ptr: &p, maxLength: length)
case 1703:
var p = pointer
transactionIdentifer = readASN1String(ptr: &p, maxLength: length)
case 1705:
var p = pointer
originalTransactionIdentifier = readASN1String(ptr: &p, maxLength: length)
case 1704:
var p = pointer
purchaseDate = readASN1Date(ptr: &p, maxLength: length)
case 1706:
var p = pointer
originalPurchaseDate = readASN1Date(ptr: &p, maxLength: length)
case 1708:
var p = pointer
subscriptionExpirationDate = readASN1Date(ptr: &p, maxLength: length)
case 1712:
var p = pointer
subscriptionCancellationDate = readASN1Date(ptr: &p, maxLength: length)
case 1711:
var p = pointer
webOrderLineId = readASN1Integer(ptr: &p, maxLength: length)
default:
break
}
pointer = pointer!.advanced(by: length)
}
}
The only difference from the code reading the initial set comes from the different type values found in an in-app purchase. If at any point in the initialization it finds an unexpected value, it returns nil
and stops.
Back in Receipt.swift, replace the switch case 17: // IAP Receipt
in readReceipt(_:)
with the following to use the new objects:
case 17: // IAP Receipt
var iapStartPtr = ptr
let parsedReceipt = IAPReceipt(with: &iapStartPtr, payloadLength: length)
if let newReceipt = parsedReceipt {
inAppReceipts.append(newReceipt)
}
You pass the current pointer to init()
to read the set containing the IAP. If a valid receipt item comes back, it's added to the array. Note that for consumable and non-renewing subscriptions, in-app purchases only appear once at the time of purchase. They are not included in future receipt updates. Non-consumable and auto-renewing subscriptions will always show in the receipt.
Validating the Receipt
With the receipt payload read, you can finish validating the receipt. Add this code to init()
in Receipt
:
validateReceipt()
Add a new method to Receipt
:
private func validateReceipt() {
guard
let idString = bundleIdString,
let version = bundleVersionString,
let _ = opaqueData,
let hash = hashData
else {
receiptStatus = .missingComponent
return
}
}
This code ensures the receipt contains the elements required for validation. If any are missing, validation fails. Add the following code at the end of validateReceipt()
:
// Check the bundle identifier
guard let appBundleId = Bundle.main.bundleIdentifier else {
receiptStatus = .unknownFailure
return
}
guard idString == appBundleId else {
receiptStatus = .invalidBundleIdentifier
return
}
This code gets the bundle identifier of your app and compares it to the bundle identifier in the receipt. If they don't match, the receipt is likely copied from another app and not valid.
Add the following code after the validation of the identifier:
// Check the version
guard let appVersionString =
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else {
receiptStatus = .unknownFailure
return
}
guard version == appVersionString else {
receiptStatus = .invalidVersionIdentifier
return
}
You compare the version stored in the receipt to the current version of your app. If the values don't match, the receipt likely was copied from another version of the app, as the receipt should be updated with the app.
The final validation check validates that the receipt was created for the current device. To do this, you need the device identifier, an alphanumeric string that uniquely identifies a device for your app.
Add the following method to Receipt
:
private func getDeviceIdentifier() -> Data {
let device = UIDevice.current
var uuid = device.identifierForVendor!.uuid
let addr = withUnsafePointer(to: &uuid) { (p) -> UnsafeRawPointer in
UnsafeRawPointer(p)
}
let data = Data(bytes: addr, count: 16)
return data
}
This method gets the device identifier as a Data
object.
You validate the device using a hash function. A hash function is easy to compute in one direction but difficult to reverse. A hash is commonly used to allow confirmation of a value without the need to store the value itself. For example, passwords are normally stored as hashed values instead of the actual password. Several values can be hashed together, and if the end result is the same, you can feel confident that the original values were the same.
Add the following method at the end of the Receipt
class:
private func computeHash() -> Data {
let identifierData = getDeviceIdentifier()
var ctx = SHA_CTX()
SHA1_Init(&ctx)
let identifierBytes: [UInt8] = .init(identifierData)
SHA1_Update(&ctx, identifierBytes, identifierData.count)
let opaqueBytes: [UInt8] = .init(opaqueData!)
SHA1_Update(&ctx, opaqueBytes, opaqueData!.count)
let bundleBytes: [UInt8] = .init(bundleIdData!)
SHA1_Update(&ctx, bundleBytes, bundleIdData!.count)
var hash: [UInt8] = .init(repeating: 0, count: 20)
SHA1_Final(&hash, &ctx)
return Data(bytes: hash, count: 20)
}
You compute a SHA-1 hash to validate the device. The OpenSSL libraries again can compute the SHA-1 hash you need. You combine the opaque value from the receipt, the bundle identifier in the receipt, and the device identifier. Apple knows these values at the time of purchase and your app knows them at the time of verification. By computing the hash and checking against the one in the receipt, you validate the receipt was created for the current device.
Add the following code to the end of validateReceipt()
:
// Check the GUID hash
let guidHash = computeHash()
guard hash == guidHash else {
receiptStatus = .invalidHash
return
}
This code compares the calculated hash to the value in the receipt. If they do not match, the receipt likely was copied from another device and is invalid.
The final check for a receipt only applies to apps allowing Volume Purchase Program (VPP) purchases. These purchases include an expiration date in the receipt. Add the following code to finish out validateReceipt()
:
// Check the expiration attribute if it's present
let currentDate = Date()
if let expirationDate = expirationDate {
if expirationDate < currentDate {
receiptStatus = .invalidExpired
return
}
}
// All checks passed so validation is a success
receiptStatus = .validationSuccess
If there is a non-nil expiration date, then your app should check that the expiration falls after the current date. If it's before the current date, the receipt is no longer valid. If no expiration date exists, then the validation does not fail.
At last, having completed all these checks without any failure, you can mark the receipt as valid.
Running the App
Run the app. You must run this project on a real device. Store related code won't work in the simulator. You'll also need a sandbox account set-up as described in In-App Purchase Tutorial: Getting Started. In an App Store purchased app, a receipt would be present. But when testing from XCode, you'll need to refresh to get a receipt. The tutorial app already does this. You'll need to log in. The app will then use the code from this tutorial to verify the receipt and display the information from it.
Once you've done this, add in-app purchases as described in the IAP tutorial. Make sure to also update ViewController.swift with the product identifiers. Use the Buy IAP button and a sandbox account. You'll see that the table view lists these in-app purchases. Also try consumable purchases, and note how they disappear after you refresh the receipt.
Protecting Receipt Validation Code
Attackers will work to bypass your receipt validation code. Using this or any other receipt validation code without change incurs risk. If an attacker can bypass the check in one app that uses this exact code, the attacker can more easily repeat the process for another app with the same code. For a high value or high profile app, you'll want to make modifications to the code of this tutorial while keeping the same work.
To defend against bypassing the validation process, you can perform the validation repeatedly instead of just once. Avoiding explicit error messages such as "Receipt Validation Failed" makes the attacker's job more difficult. Placing failure code in a section of your app away from the validation check also makes the attacker's job more difficult.
In the end, you will need to balance the risks of unauthorized access to your app against the extra time and complexity the additional obfuscation of the code adds to your development process.
Where to Go From Here?
You can download the completed project using the Download Materials button at the top or bottom of the tutorial. Use the provided code as a start to implement receipt validation in your own app projects.
Apple's Receipt Validation Programming Guide offers the best documentation on receipts, along with a WWDC 2014 session on Preventing Unauthorized Purchases with Receipts. Both discuss the server validation method not addressed in this tutorial. A session from WWDC 2016, Using Store Kit for In-App Purchases with Swift 3, also discusses receipts especially related to subscriptions.
And for more about In-App Purchases, see In-App Purchase Tutorial: Getting Started.