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
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.