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