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