In-App Purchases: Receipt Validation Tutorial

0
10

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.

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:

  1. 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.
  2. Initialize OpenSSL for certificate validation.
  3. Use PKCS7_verify(_:_:_:_:_:_:) to verify a certificate in the chain from the root certificate signed the receipt. If so, the function returns 1. 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:

  1. Create a loop that runs until the pointer reaches the end of the payload. At that point you’ve processed the entire payload.
  2. Check that the object is a sequence. Each attribute is a sequence of three fields: type, version, data.
  3. Fetch the attribute type — an integer — that you’ll use shortly.
  4. Read the attribute version, an integer. You won’t need it for receipt validation.
  5. 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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here