Skip to content

Instantly share code, notes, and snippets.

@mattstevens
Created October 19, 2011 02:04
Show Gist options
  • Save mattstevens/1297304 to your computer and use it in GitHub Desktop.
Save mattstevens/1297304 to your computer and use it in GitHub Desktop.
An insecure but simple method of validating Mac App Store receipts using only Security.framework.
#import <Cocoa/Cocoa.h>
BOOL VerifyAppStoreReceipt();
BOOL VerifyAppStoreReceiptData(NSData *data);
NSURL *BackupReceiptURL();
void BackupAppStoreReceipt();
NSData *MACAddressData();
NSDictionary *DictionaryFromAppStoreReceipt(NSData *fullData);
#import "AppStoreReceipt.h"
#import <Security/Security.h>
#import <Security/SecAsn1Coder.h>
#import <Security/SecAsn1Templates.h>
#include <CommonCrypto/CommonDigest.h>
NSString *kReceiptBundleIdentifier = @"BundleIdentifier";
NSString *kReceiptBundleIdentifierData = @"BundleIdentifierData";
NSString *kReceiptVersion = @"Version";
NSString *kReceiptOpaqueValue = @"OpaqueValue";
NSString *kReceiptHash = @"Hash";
enum ATTRIBUTES
{
ATTR_START = 1,
BUNDLE_ID,
VERSION,
OPAQUE_VALUE,
HASH,
ATTR_END
};
typedef struct {
CSSM_SIZE length;
uint8_t *data;
} Asn1Data;
typedef struct {
Asn1Data type;
Asn1Data version;
Asn1Data value;
} ReceiptAttribute;
typedef struct {
ReceiptAttribute** attributes;
} Receipt;
const SecAsn1Template kReceiptAttributeTemplate[] = {
{ SEC_ASN1_SEQUENCE, 0, NULL, sizeof(ReceiptAttribute) },
{ SEC_ASN1_INTEGER, offsetof(ReceiptAttribute, type), },
{ SEC_ASN1_INTEGER, offsetof(ReceiptAttribute, version), },
{ SEC_ASN1_OCTET_STRING, offsetof(ReceiptAttribute, value), },
{ 0 }
};
const SecAsn1Template kReceiptTemplate[] = {
{ SEC_ASN1_SET_OF, offsetof(Receipt, attributes), kReceiptAttributeTemplate, sizeof(Receipt) }
};
BOOL VerifyAppStoreReceipt() {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSURL *backupURL = BackupReceiptURL();
NSURL *url = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:url];
if (!receiptData) {
// Try the backup receipt
url = backupURL;
receiptData = [NSData dataWithContentsOfURL:url];
}
BOOL result = VerifyAppStoreReceiptData(receiptData);
[pool release];
return result;
}
BOOL VerifyAppStoreReceiptData(NSData *data) {
NSDictionary *receipt = DictionaryFromAppStoreReceipt(data);
if (!receipt)
return NO;
NSData *addressData = MACAddressData();
NSMutableData *input = [NSMutableData data];
[input appendData:addressData];
[input appendData:[receipt objectForKey:kReceiptOpaqueValue]];
[input appendData:[receipt objectForKey:kReceiptBundleIdentifierData]];
NSMutableData *hash = [NSMutableData dataWithLength:CC_SHA1_DIGEST_LENGTH];
CC_SHA1([input bytes], [input length], [hash mutableBytes]);
if ([hash isEqualToData:[receipt objectForKey:kReceiptHash]] == NO) {
return NO;
}
return YES;
}
NSURL *BackupReceiptURL() {
NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"];
NSString *backupPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
backupPath = [backupPath stringByAppendingPathComponent:appName];
backupPath = [backupPath stringByAppendingPathComponent:@"receipt"];
return [NSURL fileURLWithPath:backupPath];
}
void BackupAppStoreReceipt() {
NSURL *url = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *data = [NSData dataWithContentsOfURL:url];
NSURL *backupURL = BackupReceiptURL();
NSData *backupData = [NSData dataWithContentsOfURL:backupURL];
if (data && [backupData isEqualToData:data] == NO) {
NSURL *appSupportURL = [backupURL URLByDeletingLastPathComponent];
[[NSFileManager defaultManager] createDirectoryAtURL:appSupportURL withIntermediateDirectories:YES attributes:nil error:nil];
[data writeToURL:backupURL atomically:YES];
}
}
NSData *MACAddressData() {
kern_return_t kernResult;
mach_port_t master_port;
CFMutableDictionaryRef matchingDict;
io_iterator_t iterator;
io_object_t service;
CFDataRef macAddress = nil;
kernResult = IOMasterPort(MACH_PORT_NULL, &master_port);
if (kernResult != KERN_SUCCESS) {
return nil;
}
matchingDict = IOBSDNameMatching(master_port, 0, "en0");
if (!matchingDict) {
return nil;
}
kernResult = IOServiceGetMatchingServices(master_port, matchingDict, &iterator);
if (kernResult != KERN_SUCCESS) {
return nil;
}
while((service = IOIteratorNext(iterator)) != 0) {
io_object_t parentService;
kernResult = IORegistryEntryGetParentEntry(service, kIOServicePlane, &parentService);
if (kernResult == KERN_SUCCESS) {
if (macAddress) CFRelease(macAddress);
macAddress = (CFDataRef)IORegistryEntryCreateCFProperty(parentService, CFSTR("IOMACAddress"), kCFAllocatorDefault, 0);
IOObjectRelease(parentService);
}
IOObjectRelease(iterator);
IOObjectRelease(service);
}
return [(NSData *)macAddress autorelease];
}
NSDictionary *DictionaryFromAppStoreReceipt(NSData *fullData) {
OSStatus err;
CMSDecoderRef decoder;
err = CMSDecoderCreate(&decoder);
if (err != noErr)
return nil;
err = CMSDecoderUpdateMessage(decoder, [fullData bytes], [fullData length]);
if (err != noErr)
return nil;
err = CMSDecoderFinalizeMessage(decoder);
if (err != noErr)
return nil;
CFDataRef dataRef;
CMSDecoderCopyContent(decoder, &dataRef);
NSData *receiptData = [(NSData *)dataRef autorelease];
CFRelease(decoder);
NSMutableDictionary *info = [NSMutableDictionary dictionary];
SecAsn1CoderRef coder;
SecAsn1CoderCreate(&coder);
Receipt receipt = {0};
err = SecAsn1Decode(coder, [receiptData bytes], [receiptData length], kReceiptTemplate, &receipt);
if (err != noErr || receipt.attributes == NULL)
return nil;
for (int i = 0; receipt.attributes[i] != NULL; i++) {
ReceiptAttribute *attr = receipt.attributes[i];
if (attr->type.length < 1 || attr->value.length < 1)
continue;
NSString *key;
int type = attr->type.data[0];
if (type <= ATTR_START || type >= ATTR_END)
continue;
// Bytes
if (type == BUNDLE_ID || type == OPAQUE_VALUE || type == HASH) {
NSData *data = [NSData dataWithBytes:attr->value.data length:(NSUInteger)attr->value.length];
switch (type) {
case BUNDLE_ID:
// Included for hash generation
key = kReceiptBundleIdentifierData;
break;
case OPAQUE_VALUE:
key = kReceiptOpaqueValue;
break;
case HASH:
key = kReceiptHash;
break;
}
[info setObject:data forKey:key];
}
// Strings
if (type == BUNDLE_ID || type == VERSION) {
Asn1Data raw_string;
err = SecAsn1Decode(coder, attr->value.data, attr->value.length, kSecAsn1UTF8StringTemplate, &raw_string);
if (err == 0) {
NSString *string = [[[NSString alloc] initWithBytes:raw_string.data
length:(NSUInteger)raw_string.length
encoding:NSUTF8StringEncoding] autorelease];
switch (type) {
case BUNDLE_ID:
key = kReceiptBundleIdentifier;
break;
case VERSION:
key = kReceiptVersion;
break;
}
[info setObject:string forKey:key];
}
}
}
SecAsn1CoderRelease(coder);
return info;
// This takes a while, should obtain secTrustOut and verify asynchronously or copy the certs from
// the CMS message and look for an Apple root.
/*SecPolicyRef policy = SecPolicyCreateWithOID(kSecPolicyMacAppStoreReceipt); //SecPolicyCreateBasicX509();
CMSSignerStatus signerStatus = 0;
SecTrustRef secTrust = NULL;
OSStatus verifyResultCode = 0;
err = CMSDecoderCopySignerStatus(decoder, 0, policy, TRUE, &signerStatus, NULL, &verifyResultCode);*/
}
@mattstevens
Copy link
Author

You're right, plus the code should verify that the receipt was signed by Apple. These are worthwhile and easy to add, but my goal is just a basic check that prevents otherwise honest people from copying the app and doesn't require asn1c or OpenSSL. If someone is going to swap receipts or re-sign things they can probably find another way to steal the app.

It's also worth noting that because this implementation depends entirely on Security.framework it is vulnerable to library swapping. If an attacker caused the app to load their own version of the Security framework they could bypass the whole check.

@theMikeSwan
Copy link

I was mostly thinking that it would be super easy to grab a free app from the store and then just copy the receipt into other packages at will. It was super easy to add checks for the bundle ID and version (that was the part I understood, the rest is still beyond me).
Thanks for posting this code, I'm now using a slightly altered version (combined the two BOOL functions into a block) of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment