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