Tooth Fairy 2.0 Released!

I just released Tooth Fairy 2.0. The core change of this release is that now Tooth Fairy supports multiple devices.

Multiple device support is one of the most requested features. The first release of Tooth Fairy was a quick implementation to meet a simple requirement. In 2.0, I rewrote most part of the code to fit for the multiple device management.

“making life just a tiny bit easier” is one of my most favorite reviews from all the lovely users leaving reviews on Mac App Store. Hoping the new release is still getting a tiny bit better.

Local Receipt Validation in Swift 3 Part II - Validation

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
struct IAPurchaseReceipt {
    var quantity: Int? = nil
    var productIdentifier: String? = nil
    var transactionIdentifier: String? = nil
    var originalTransactionIdentifier: String? = nil
    var purchaseDate: Date? = nil
    var originalPurchaseDate: Date? = nil
    var subscriptionExpirationDate: Date? = nil
    var cancellationDate: Date? = nil
    var webOrderLineItemID: Int? = nil

    init(with asn1Data: UnsafePointer<UInt8>, len: Int) {
        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)
    } else if type == V_ASN1_IA5STRING {
        let p = UnsafeMutableRawPointer(mutating: ptr!)
        return String(bytesNoCopy: p, length: strLength, encoding: String.Encoding.ascii, 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