Local Receipt Validation in Swift 3 Part II - Validate

The receipt validation consists of following steps:

  1. Verify if the receipt is issued by Apple
  2. Verify if the receipt is valid for the app
  3. If you want to verify an in-app-purchase, you need to verify if the IAP receipt is valid.

Let’s go through the process step by step.

Verify the source

First, we need to verify that the receipt is signed by Apple.

guard let receiptURL = Bundle.main.appStoreReceiptURL,
            let certificateURL = Bundle.main.url(forResource: "AppleIncRootCertificate", withExtension: "cer"),
            let receiptData = NSData(contentsOf: receiptURL),
            let certificateData = NSData(contentsOf: certificateURL) else {
    throw ReceiptError.invalidReceipt
}
let bio = BIOWrapper(data: receiptData)
let p7 : UnsafeMutablePointer<PKCS7> = d2i_PKCS7_bio(bio.bio, nil)
if p7 == nil {
    throw ReceiptError.unexpected
}
OpenSSL_add_all_digests()

let x509Store = X509StoreWrapper()
let certificate = X509Wrapper(data: certificateData)
x509Store.addCert(x509: certificate)
let payload = BIOWrapper()
guard PKCS7_verify(p7!, nil, x509Store.store, nil, payload.bio, 0) == 1 else {
    throw ReceiptError.invalidReceipt
}

Verify the receipt

Now we begin to parse the receipt and get following information from the receipt:

  • bundle Identifier
  • bundle Version
  • hash data
  • in-app-purchase receipts, if you have
if let contents = p7.pointee.d.sign.pointee.contents,
    OBJ_obj2nid(contents.pointee.type) == NID_pkcs7_data ,
    let octets = contents.pointee.d.data {
    var ptr : UnsafePointer? = UnsafePointer(octets.pointee.data)
    let end = ptr!.advanced(by: Int(octets.pointee.length))
    var type : Int32 = 0
    var xclass: Int32 = 0
    var length = 0
    ASN1_get_object(&ptr, &length, &type, &xclass,Int(octets.pointee.length))
    guard type == V_ASN1_SET else {
        return
    }
    while ptr! < end {
        ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
        guard type == V_ASN1_SEQUENCE else {
            return
        }
        
        guard let attrType = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
            return
        }
        
        guard let _ = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
            return
        }
        
        ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
        guard type == V_ASN1_OCTET_STRING else {
            return
        }
        
        switch attrType {
        case 2:
            var strPtr = ptr
            self.bundleIdData = NSData(bytes: strPtr, length: length)
            self.bundleIdString = ASN1ReadString(pointer: &strPtr, length: length)
        case 3:
            var strPtr = ptr
            self.bundleVersionString = ASN1ReadString(pointer: &strPtr, length: length)
        case 4:
            self.opaqueData = NSData(bytes: ptr!, length: length)
        case 5:
            self.hashData = NSData(bytes: ptr!, length: length)
        case 17:
            let p = ptr
            let iapReceipt = IAPurchaseReceipt(with: p!, len: length)
            self.iapReceipts.append(iapReceipt)
        case 21:
            var strPtr = ptr
            self.expirationDate = ASN1ReadDate(pointer: &strPtr, length: length)
        default:
            break
        }
        ptr = ptr?.advanced(by: length)
    }
}

After the parsing done, check if the bundle identifier and version are the same as the ones of the app. You also need to check the hash data. Before checking, you need to compute a hash data with the device identifier.

func computedHashData() -> NSData {
    let device = UIDevice.current
    var uuid = device.identifierForVendor?.uuid
    let address = withUnsafePointer(to: &uuid) {UnsafeRawPointer($0)}
    let data = NSData(bytes: address, length: 16)
    var hash = Array<UInt8>(repeating: 0, count: 20)
    var ctx = SHA_CTX()
    SHA1_Init(&ctx)
    SHA1_Update(&ctx, data.bytes, data.length)
    SHA1_Update(&ctx, opaqueData!.bytes, opaqueData!.length)
    SHA1_Update(&ctx, bundleIdData!.bytes, bundleIdData!.length)
    SHA1_Final(&hash, &ctx)
    return NSData(bytes: &hash, length: 20)
}

and then check if it is the same as the one from the receipt.

Verify IAP receipts

IAP receipts are stored in the receipt data and parsed during last step. There could be one or more IAP receipts in the receipt data.

To verify each IAP receipt, we also need to parse the IAP receipt data and get following information from it:

  • quantity
  • product identifier
  • transaction identifier
  • original transaction identifier
  • purchase date
  • original purchase data
  • subscription expiration date
  • cancellation date
  • web order line item ID
var ptr : UnsafePointer<UInt8>? = asn1Data
let end = asn1Data.advanced(by: len)
var type : Int32 = 0
var xclass: Int32 = 0
var length = 0
ASN1_get_object(&ptr, &length, &type, &xclass,Int(len))
guard type == V_ASN1_SET else {
    return
}
while ptr! < end {
    ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
    guard type == V_ASN1_SEQUENCE else {
        return
    }
    
    guard let attrType = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
        return
    }

    guard let _ = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
        return
    }
    
    ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
    guard type == V_ASN1_OCTET_STRING else {
        return
    }
    
    switch attrType {
    case 1701:
        var p = ptr
        self.quantity = ASN1ReadInteger(pointer: &p , length: length)
    case 1702:
        var p = ptr
        self.productIdentifier = ASN1ReadString(pointer: &p, length: length)
    case 1703:
        var p = ptr
        self.transactionIdentifier = ASN1ReadString(pointer: &p, length: length)
    case 1705:
        var p = ptr
        self.originalTransactionIdentifier = ASN1ReadString(pointer: &p, length: length)
    case 1704:
        var p = ptr
        self.purchaseDate = ASN1ReadDate(pointer: &p, length: length)
    case 1706:
        var p = ptr
        self.originalPurchaseDate = ASN1ReadDate(pointer: &p, length: length)
    case 1708:
        var p = ptr
        self.subscriptionExpirationDate = ASN1ReadDate(pointer: &p, length: length)
    case 1712:
        var p = ptr
        self.cancellationDate = ASN1ReadDate(pointer: &p, length: length)
    case 1711:
        var p = ptr
        self.webOrderLineItemID = ASN1ReadInteger(pointer: &p, length: length)
    default:
        break
    }
    ptr = ptr?.advanced(by: length)
}

After the parsing, you can check if the information is correct according to your own IAP setup.

Table of Content

  1. Introduction
  2. Preparation
  3. Validation

Local Receipt Validation in Swift 3 Part I - Preparation

Before starting validation, we need some preparation for the dependencies.

OpenSSL

The implementation is based on OpenSSL. It would be better to use a self-build OpenSSL linked to your app statically.

To build the OpenSSL library, please follow the instruction in OpenSSL-for-iPhone.

Bitcode has been supported since August, 2015, which makes it easier for Swift.

Add openssl to your project

After you build the OpenSSL library, you need to add it into your project:

  1. Copy the lib and include folder in the OpenSSL build to, for example, the folder external/openssl of your app project.
  2. Add libssl.a and libcrtpto.a as Linked Frameworks and Libraries in General tab in the target settings of your app.
  3. Add the library path to Library Search Paths in Build Settings as, for example, $(PROJECT_DIR)/external/openssl
  4. Add the include path to Header Search Paths in Build Settings of both your app target and unit test target.
  5. Add following lines to the bridge header file:
#import <openssl/pkcs7.h>
#import <openssl/objects.h>
#import <openssl/evp.h>
#import <openssl/ssl.h>

I encountered a compiling error with the version 1.0.2j of OpenSSL library. An upper case I was used in a function signature definition in rsa.h but an I has already been defined somewhere else at /usr/include/complex.h. I worked it around by changing the line from:

 /* Can be null */
int (*rsa_mod_exp) (BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx);

to:

/* Can be null */
int (*rsa_mod_exp) (BIGNUM *r0, const BIGNUM *i, RSA *rsa, BN_CTX *ctx);

Import Apple Inc. Root Certificate

You need the root certificate from Apple to validate the receipt. Download it from https://www.apple.com/certificateauthority/ and add the cer file to the target of your app. Make sure it is copied into the resource of the final bundle.

Reachability of Receipt

On an iOS device, the receipt can be located at Bundle.main.appStoreReceiptURL. You can use following lines to check if it is available:

if let receiptURL = Bundle.main.appStoreReceiptURL, let isReachable = try? receiptURL.checkResourceIsReachable(), isReachable {
	// Receipt available
	...
} 

If the receipt is not reachable, you can issue a refresh request to the app store and set the request delegate.

self.receiptRefreshRequest = SKReceiptRefreshRequest()
self.receiptRefreshRequest.delegate = self
self.receiptRefreshRequest.start()

When the request is finished or failed, following methods in the delegate will be called accordingly:

func request(_ request: SKRequest, didFailWithError error: Error) {
		...
}

func requestDidFinish(_ request: SKRequest) {
    ...
}

## Prepare for parsing

The receipts are stored in a PKCS #7 container and encoded using ASN.1. We need to use some C API to parse the data.

I composed some wrapper for the C structs to simplify the memory management.

class BIOWrapper {
    let bio = BIO_new(BIO_s_mem())
    init(data:NSData) {
        BIO_write(bio, data.bytes, Int32(data.length))
    }
    
    init() {}
    
    deinit {
        BIO_free(bio)
    }
}

class X509StoreWrapper {
    let store = X509_STORE_new()
    deinit {
        X509_STORE_free(store)
    }

    func addCert(x509:X509Wrapper) {
        X509_STORE_add_cert(store, x509.x509)
    }
}

class X509Wrapper {
    let x509 : UnsafeMutablePointer<X509>!
    init(data:NSData){
        let certBIO = BIOWrapper(data: data)
        x509 = d2i_X509_bio(certBIO.bio, nil)
    }
    
    deinit {
        X509_free(x509)
    }
}

and some helper function to parse the ASN.1 structure:

func ASN1ReadInteger(pointer ptr: inout UnsafePointer<UInt8>?, length:Int) -> Int? {
    var type : Int32 = 0
    var xclass: Int32 = 0
    var len = 0
    ASN1_get_object(&ptr, &len, &type, &xclass, length)
    guard type == V_ASN1_INTEGER else {
        return nil
    }
    let integer = c2i_ASN1_INTEGER(nil, &ptr, len)
    let result = ASN1_INTEGER_get(integer)
    ASN1_INTEGER_free(integer)
    return result
}

func ASN1ReadString(pointer ptr: inout UnsafePointer<UInt8>?, length:Int) -> String? {
    var strLength = 0
    var type : Int32 = 0
    var xclass: Int32 = 0
    ASN1_get_object(&ptr, &strLength, &type, &xclass, length)
    if type == V_ASN1_UTF8STRING {
        let p = UnsafeMutableRawPointer(mutating: ptr!)
        return String(bytesNoCopy: p, length: strLength, encoding: String.Encoding.utf8, freeWhenDone: false)
    }
    return nil
}

func ASN1ReadDate(pointer ptr: inout UnsafePointer<UInt8>?, length:Int) -> Date? {
    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
    dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
    if let dateString = ASN1ReadString(pointer: &ptr, length:length) {
        return dateFormatter.date(from: dateString)
    }
    return nil
}

Table of Content

  1. Introduction
  2. Preparation
  3. Validation

Local Receipt Validation in Swift 3

When using StoreKit, there are two ways to validate a receipt. One is to validate with the App Store. It’s recommended if you are doing it from a trusted server. The procedure is straightforward and easy to implement. However, if you want to validate a receipt from the device directly, while “a trusted connection between a user’s device and the App Store directly” is not possible as stated by Apple, you’d better to choose the other way, which is to validate the receipt locally.

The local validation is not as easy as to validate with the App Store service. Meanwhile, Apple believe the approach should not base on the same implementation due to security consideration and doesn’t provided a complete sample code for you to copy and paste, which makes it even harder. objc.io had a wonderful article on how to do it in Objective-C. There are also some articles about how to do it in Swift but are either incomplete or obsolete somehow.

In this series, I kept a record on how I made it for iOS in Swift 3.

The implementation is under Xcode 8 with iOS 10 SDK. The ultimate document from Apple about the this can be found at General->Guides->Receipt Validation Programming Guide. I won’t leave a link here since it changes all the time. Just check it out in Xcode.

  1. Preparation
  2. Validation