Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-05-20

OSX Receipt validation in Swift, part 6: Example code

Without further ado, here is complete code listing:

//
//  main.swift
//

import Cocoa
import IOKit

let errorCode: Int32 = 173

// Check if receipt is available

let mainBundle = NSBundle.mainBundle()
let receiptURL = mainBundle.appStoreReceiptURL
var error: NSError?
if (receiptURL?.checkResourceIsReachableAndReturnError(&error) == nil) {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt is not available")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt is available")


// Loading the receipt file

let receiptBIO = BIO_new(BIO_s_mem())
if let receiptData = NSData(contentsOfURL: receiptURL!) {
    BIO_write(receiptBIO, receiptData.bytes, Int32(receiptData.length))
} else {
    log.atLevelDebug(id: 0, source: "Main", message: "Could not read receipt data")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt data read")


// Parse the PKCS7 envelope

let receiptPKCS7 = d2i_PKCS7_bio(receiptBIO, nil)

if receiptPKCS7 == nil {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt PKCS7 container parsing error")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt PKCS7 parsed ok")


// Check for a signature

if OBJ_obj2nid(receiptPKCS7.memory.type) != NID_pkcs7_signed {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt is not signed")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt PKCS7 is signed")


// Check for data

if OBJ_obj2nid(pkcs7_d_sign(receiptPKCS7).memory.contents.memory.type) != NID_pkcs7_data {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt does not contain signed data")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt contains signed data")


// Load the apple root certificate

let appleRootData: NSData!
if let appleRootURL = NSBundle.mainBundle().URLForResource("AppleIncRootCertificate", withExtension: "cer") {
    appleRootData = NSData(contentsOfURL: appleRootURL)
    if (appleRootData == nil) || (appleRootData.length == 0) {
        log.atLevelDebug(id: 0, source: "Main", message: "Could not load the Apple root certificate")
        exit(errorCode)
    }
} else {
    log.atLevelDebug(id: 0, source: "Main", message: "Apple root certificate URL not found")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Apple root certificate loaded")


// Verify the receipt signature

let appleRootBIO = BIO_new(BIO_s_mem())
BIO_write(appleRootBIO, appleRootData.bytes, Int32(appleRootData.length))
let appleRootX509 = d2i_X509_bio(appleRootBIO, nil)
let store = X509_STORE_new()
X509_STORE_add_cert(store, appleRootX509)
OpenSSL_add_all_digests()
let result = PKCS7_verify(receiptPKCS7, nil, store, nil, nil, 0)
if result != 1 {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt signature verification failed")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt signature verification ok")


// Extract the data set to be verified from the receipt

let octets = pkcs7_d_data(pkcs7_d_sign(receiptPKCS7).memory.contents)
var ptr = UnsafePointer<UInt8>(octets.memory.data)
let end = ptr.advancedBy(Int(octets.memory.length))

var type: Int32 = 0
var xclass: Int32 = 0
var length = 0

ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr)
if (type != V_ASN1_SET) {
    log.atLevelDebug(id: 0, source: "Main", message: "Could not read a ASN1 set from the receipt")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "An ASN1 set was read from the receipt")


// Extract the validation data from the set

var bundleIdString: NSString?
var bundleVersionString: NSString?
var bundleIdData: NSData?
var hashData: NSData?
var opaqueData: NSData?
var expirationDate: NSDate?

let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)

while (ptr < end) {
    
    var integer: UnsafeMutablePointer<ASN1_INTEGER>
    
    // Expecting an attribute sequence
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr)
    if type != V_ASN1_SEQUENCE {
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: expected an attribute sequence")
        exit(errorCode)
    }
    
    let seq_end = ptr.advancedBy(length)
    var attr_type = 0
    var attr_version = 0
    
    // The attribute is an integer
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr)
    if type != V_ASN1_INTEGER {
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: attribute not an integer")
        exit(errorCode)
    }
    integer = c2i_ASN1_INTEGER(nil, &ptr, length)
    attr_type = ASN1_INTEGER_get(integer)
    ASN1_INTEGER_free(integer)
    
    // The version is an integer
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr)
    if type != V_ASN1_INTEGER {
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: version not an integer")
        exit(errorCode)
    }
    integer = c2i_ASN1_INTEGER(nil, &ptr, length);
    attr_version = ASN1_INTEGER_get(integer);
    ASN1_INTEGER_free(integer);

    // The attribute value is an octet string
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr)
    if type != V_ASN1_OCTET_STRING {
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: value not an octet string")
        exit(errorCode)
    }

    switch (attr_type) {
    case 2:
        // Bundle identifier
        var str_ptr = ptr
        var str_type: Int32 = 0
        var str_length = 0
        var str_xclass: Int32 = 0
        ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr)
        if str_type == V_ASN1_UTF8STRING {
            // We store both the decoded string and the raw data for later
            // The raw is data will be used when computing the GUID hash
            bundleIdString = NSString(bytes: str_ptr, length: str_length, encoding: NSUTF8StringEncoding)
            bundleIdData = NSData(bytes: ptr, length: length)
            log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Read bundle identifier \(bundleIdString)")
        }
        
    case 3:
        // Bundle version
        var str_ptr = ptr
        var str_type: Int32 = 0
        var str_length = 0
        var str_xclass: Int32 = 0
        ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr)
        if str_type == V_ASN1_UTF8STRING {
            // We store the decoded string for later
            bundleVersionString = NSString(bytes: str_ptr, length: str_length, encoding: NSUTF8StringEncoding)
            log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Read bundle version \(bundleVersionString)")
        }
        break;
        
    case 4:
        // Opaque value
        opaqueData = NSData(bytes: ptr, length: length)
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Read opaque value of \(opaqueData!.length) bytes")
        
    case 5:
        // Computed GUID (SHA-1 Hash)
        hashData = NSData(bytes: ptr, length: length)
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Read hashData (GUID) of \(hashData!.length) bytes")

    case 21:
        // Expiration date
        var str_ptr = ptr
        var str_type: Int32 = 0
        var str_length = 0
        var str_xclass: Int32 = 0
        ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr)
        if str_type == V_ASN1_IA5STRING {
            // The date is stored as a string that needs to be parsed
            let dateString = String(NSString(bytes: str_ptr, length: str_length, encoding:NSASCIIStringEncoding)!)
            expirationDate = formatter.dateFromString(dateString)
            log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Read expirationDate \(dateString)")
        }
        
    default: //
        log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Ignored value of type \(attr_type)")
    }
    
    // Move past the value
    ptr = ptr.advancedBy(length)
}
log.atLevelDebug(id: 0, source: "Main", message: "ASN1: Parsing ended")


// Verify for completeness

if bundleIdString == nil {
    log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: Missing bundleIdString")
    exit(errorCode)
}
if bundleVersionString == nil {
    log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: Missing bundleVersionString")
    exit(errorCode)
}
if opaqueData == nil {
    log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: Missing opaqueData")
    exit(errorCode)
}
if hashData == nil {
    log.atLevelDebug(id: 0, source: "Main", message: "ASN1 error: Missing hashData")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "All necessary data read from receipt")


// Verify bundleIdString

if bundleIdString != "com.mycompany.MyGreatApp" {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification error: Wrong bundle identifier")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification: Bundle identifier OK")


// Verify version

if bundleVersionString != "1.0" {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification error: Wrong bundle version")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification: Bundle version OK")


// Retrieve the Device GUID

let guidDataOrNil = copy_mac_address()
if guidDataOrNil == nil {
    log.atLevelDebug(id: 0, source: "Main", message: "Could not retrieve the device GUID")
    exit(errorCode)
}
let guidData: NSData = guidDataOrNil.takeRetainedValue()
log.atLevelDebug(id: 0, source: "Main", message: "Device GUID retrieved OK")


// Verify the hash

var hash = Array<UInt8>(count: 20, repeatedValue: 0)
var ctx = SHA_CTX()
SHA1_Init(&ctx)
SHA1_Update(&ctx, guidData.bytes, guidData.length)
SHA1_Update(&ctx, opaqueData!.bytes, opaqueData!.length)
SHA1_Update(&ctx, bundleIdData!.bytes, bundleIdData!.length)
SHA1_Final(&hash, &ctx)

let computedHashData = NSData(bytes: &hash, length: 20)
if !computedHashData.isEqualToData(hashData!) {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification error: Wrong hash")
    exit(errorCode)
}
log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification: Hash OK")


// Verify the expiration data for volume purchases only

if let expDate = expirationDate {
    if expDate.compare(NSDate()) == NSComparisonResult.OrderedAscending {
        log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification error: Volume purchase, expiration date expired")
        exit(errorCode)
    }
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification: Volume purchase, expiration date OK")
} else {
    log.atLevelDebug(id: 0, source: "Main", message: "Receipt verification: No volume purchase")
}


NSApplicationMain(Process.argc, Process.unsafeArgv)

Once run, it produced this output:

2015-05-14T11:56:16.915+0200, DEBUG    : 0, Main, Receipt is available
2015-05-14T11:56:16.916+0200, DEBUG    : 0, Main, Receipt data read
2015-05-14T11:56:16.917+0200, DEBUG    : 0, Main, Receipt PKCS7 parsed ok
2015-05-14T11:56:16.917+0200, DEBUG    : 0, Main, Receipt PKCS7 is signed
2015-05-14T11:56:16.917+0200, DEBUG    : 0, Main, Receipt contains signed data
2015-05-14T11:56:28.853+0200, DEBUG    : 0, Main, Apple root certificate loaded
2015-05-14T11:56:40.226+0200, DEBUG    : 0, Main, Receipt signature verification ok
2015-05-14T11:57:03.542+0200, DEBUG    : 0, Main, An ASN1 set was read from the receipt
2015-05-14T11:59:36.863+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 8
2015-05-14T12:00:25.226+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 1
2015-05-14T12:00:53.299+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 11
2015-05-14T12:00:59.281+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 14
2015-05-14T12:01:02.153+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 15
2015-05-14T12:01:04.024+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 16
2015-05-14T12:01:13.261+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 25
2015-05-14T12:01:22.835+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 10
2015-05-14T12:01:24.472+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 13
2015-05-14T12:02:07.101+0200, DEBUG    : 0, Main, ASN1: Read bundle version Optional(1.0)
2015-05-14T12:02:17.460+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 19
2015-05-14T12:02:20.516+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 9
2015-05-14T12:02:30.259+0200, DEBUG    : 0, Main, ASN1: Read opaque value of 16 bytes
2015-05-14T12:02:54.578+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 0
2015-05-14T12:03:18.148+0200, DEBUG    : 0, Main, ASN1: Read hashData (GUID) of 20 bytes
2015-05-14T12:03:21.558+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 12
2015-05-14T12:03:23.663+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 18
2015-05-14T12:03:51.503+0200, DEBUG    : 0, Main, ASN1: Read bundle identifier Optional(com.mycompany.MyGreatApp)
2015-05-14T12:04:01.350+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 6
2015-05-14T12:04:03.221+0200, DEBUG    : 0, Main, ASN1: Ignored value of type 7
2015-05-14T12:04:06.355+0200, DEBUG    : 0, Main, ASN1: Parsing ended
2015-05-14T12:04:21.875+0200, DEBUG    : 0, Main, All necessary data read from receipt
2015-05-14T12:04:41.318+0200, DEBUG    : 0, Main, Receipt verification: Bundle identifier OK
2015-05-14T12:05:01.917+0200, DEBUG    : 0, Main, Receipt verification: Bundle version OK
2015-05-14T12:05:24.411+0200, DEBUG    : 0, Main, Device GUID retrieved OK
2015-05-14T12:06:03.672+0200, DEBUG    : 0, Main, Receipt verification: Hash OK
2015-05-14T12:06:10.627+0200, DEBUG    : 0, Main, Receipt verification: No volume purchase

The only thing I did change before copy & pasting the code was the bundle identifier, otherwise this is the code that is in "main.swift".

Note that main.swift is not always generated. The later versions of xcode omit this file in favour of a directive in AppDelegate. It is possible to remove the directive and supply a main.swift yourself.

The code above is pretty much a direct translation of the code in the article from Laurent Etiemble. With a few modifications that are inevitable since openSSL and Xcode are now later versions, and of course this is Swift.

Oh, did I mention (I did!) that you should not use the above code? While it demonstrates what needs to be done, it does not show how to obfuscate the whole process. That is something you should figure out yourselves! (Any hacker would hack the above code in less time than it takes you to fetch a cup of coffee!)

Btw: I used my own logging framework (SwifterLog). You can replace that with your own, or fetch mine from github. See the link at the right hand side.

Next up: part 7, testing the receipt validation

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment