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 thislink: 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 nlBitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqHWe don't get the world we wish for... we get the world we pay for.