MKStoreKit icon indicating copy to clipboard operation
MKStoreKit copied to clipboard

Handling [[NSBundle mainBundle] appStoreReceiptURL]

Open fbartolom opened this issue 11 years ago • 0 comments

Hullo, I am trying to port the solution to the new Receipt management by means of the appStoreReceiptURL. As in https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html now the NSData received is a bundle of a lot of information. So I used the code from the tutorial at: http://www.objc.io/issue-17/receipt-validation.html to unpack it. I managed to install the needed openSSL and some other elements but when I run it I bump into three problems:

  1. At the inception PKCS7_verify(receiptPKCS7, NULL, store, NULL, NULL, 0); returns 218570875
  2. The computed hashData and the one taken from the Receipt are different and, finally, what is the critical factor:
  3. The value I get at the last of several iteration of item 17 (in fact they are all equal) is not recognized by the iTunes Store, that returns in fact 21002

This is my function taking as input the appStoreReceiptURL integrating with MKStoreKit when possible:

-(void) processReceiptUrl:(NSURL*)receiptUrl{ BOOL success=YES; NSArray *downloads = nil;

ifdef __IPHONE_6_0

if([transaction respondsToSelector:@selector(downloads)])
    downloads = transaction.downloads;

if([downloads count] > 0) {

    [[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];
    // We don't have content yet, and we can't finish the transaction

ifndef NDEBUG

    NSLog(@"Download(s) started: %@", [transaction description]);

endif

    return;
}

endif

NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
// Create a memory buffer to extract the PKCS #7 container
BIO *receiptBIO = BIO_new(BIO_s_mem());
BIO_write(receiptBIO, [receiptData bytes], (int) [receiptData length]);
PKCS7 *receiptPKCS7 =d2i_PKCS7_bio(receiptBIO, NULL);
if (!receiptPKCS7) {
    NSLog(@"no receipt");
    success=NO;
}

// Check that the container has a signature
if (!PKCS7_type_is_signed(receiptPKCS7)) {
    NSLog(@"unsigned");
}

// Check that the signed container has actual data
if (!PKCS7_type_is_data(receiptPKCS7->d.sign->contents)) {
   success=NO;
}
// Load the Apple Root CA (downloaded from https://www.apple.com/certificateauthority/)
NSURL *appleRootURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"];
NSData *appleRootData = [NSData dataWithContentsOfURL:appleRootURL];
BIO *appleRootBIO = BIO_new(BIO_s_mem());
BIO_set_mem_eof_return(appleRootBIO, 0);
BIO_write(appleRootBIO, (const void *) [appleRootData bytes], (int) [appleRootData length]);
X509 *appleRootX509 = d2i_X509_bio(appleRootBIO, NULL);

// Create a certificate store
X509_STORE *store = X509_STORE_new();
X509_STORE_add_cert(store, appleRootX509);

// Be sure to load the digests before the verification
OpenSSL_add_all_digests();
//OpenSSL_add_all_algorithms();
// Check the signature
int result = PKCS7_verify(receiptPKCS7, NULL, store, NULL, NULL, 0);
if (result != 1) {
    NSLog(@"error verify: %lu", ERR_get_error());
    //success=NO;
}

ASN1_OCTET_STRING *octets = receiptPKCS7->d.sign->contents->d.data;
const unsigned char *ptr = octets->data;
const unsigned char *end = ptr + octets->length;
const unsigned char *str_ptr;

int type = 0, str_type = 0;
int xclass = 0, str_xclass = 0;
long length = 0, str_length = 0;

// Store for the receipt information
NSString *bundleIdString = nil;
NSString *bundleVersionString = nil;
NSData *bundleIdData = nil;
NSData *hashData = nil;
NSData *opaqueData = nil;
NSDate *expirationDate = nil;

// Date formatter to handle RFC 3339 dates in GMT time zone
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];

// Decode payload (a SET is expected)
ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
if (type != V_ASN1_SET) {
    success=NO;
}
NSData* iTunesValidationReceipt;
while (ptr < end) {
    ASN1_INTEGER *integer;

    // Parse the attribute sequence (a SEQUENCE is expected)
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
    if (type != V_ASN1_SEQUENCE) {
        success=NO;
    }

    const unsigned char *seq_end = ptr + length;
    long attr_type = 0;
    long attr_version = 0;

    // Parse the attribute type (an INTEGER is expected)
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
    if (type != V_ASN1_INTEGER) {
        success=NO;
    }
    integer = c2i_ASN1_INTEGER(NULL, &ptr, length);
    attr_type = ASN1_INTEGER_get(integer);
    ASN1_INTEGER_free(integer);

    // Parse the attribute version (an INTEGER is expected)
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
    if (type != V_ASN1_INTEGER) {
        success=NO;
    }
    integer = c2i_ASN1_INTEGER(NULL, &ptr, length);
    attr_version = ASN1_INTEGER_get(integer);
    ASN1_INTEGER_free(integer);

    // Check the attribute value (an OCTET STRING is expected)
    ASN1_get_object(&ptr, &length, &type, &xclass, end - ptr);
    if (type != V_ASN1_OCTET_STRING) {
        success=NO;
    }
    NSLog(@"attrtype=%ld", attr_type);
    switch (attr_type) {
        case 2:
            // Bundle identifier
            str_ptr = ptr;
            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 alloc] initWithBytes:str_ptr length:str_length encoding:NSUTF8StringEncoding];
                bundleIdData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
            }
            break;

        case 3:
            // Bundle version
            str_ptr = ptr;
            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 alloc] initWithBytes:str_ptr length:str_length encoding:NSUTF8StringEncoding];
            }
            break;

        case 4:
            // Opaque value
            opaqueData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
            break;

        case 5:
            // Computed GUID (SHA-1 Hash)
            hashData = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
            break;

        case 17:
            str_ptr = ptr;
            ASN1_get_object(&str_ptr, &str_length, &str_type, &str_xclass, seq_end - str_ptr);
            iTunesValidationReceipt = [[NSData alloc] initWithBytes:(const void *)ptr length:length];
            NSLog(@"iTunes certificate lenght=%ld %@", str_length, iTunesValidationReceipt);
            break;

        case 21:
            // Expiration date
            str_ptr = ptr;
            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
                NSString *dateString = [[NSString alloc] initWithBytes:str_ptr length:str_length encoding:NSASCIIStringEncoding];
                expirationDate = [formatter dateFromString:dateString];
            }
            break;

            // You can parse more attributes...

        default:
            break;
    }

    // Move past the value
    ptr += length;
}
// Be sure that all information is present
if (bundleIdString == nil ||
    bundleVersionString == nil ||
    opaqueData == nil ||
    hashData == nil) {
    success=NO;
}

NSString *appId=[[NSBundle mainBundle] bundleIdentifier];
NSLog(@"prodName=%@ bundleName=%@", appId, bundleIdString);
if (![bundleIdString isEqualToString:appId]) {
    NSLog(@"not my app");
    success=NO;
}
// Check the bundle version
NSString *majorVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
NSLog(@"majorVersion=%@ bundleVersion=%@", majorVersion, bundleVersionString);
if (![bundleVersionString isEqualToString:majorVersion]) {
    NSLog(@"not current release");
    success=NO;
}
UIDevice *device = [UIDevice currentDevice];
NSUUID *uuid = [device identifierForVendor];
NSData *guidData = [NSData dataWithBytes:(const void *)uuid length:16];
unsigned char hash[20];

// Create a hashing context for computation
SHA_CTX ctx;
SHA1_Init(&ctx);
SHA1_Update(&ctx, [guidData bytes], (size_t) [guidData length]);
SHA1_Update(&ctx, [opaqueData bytes], (size_t) [opaqueData length]);
SHA1_Update(&ctx, [bundleIdData bytes], (size_t) [bundleIdData length]);
SHA1_Final(hash, &ctx);

// Do the comparison
NSData *computedHashData = [NSData dataWithBytes:hash length:20];
NSLog(@"%@\n-----%@", computedHashData, hashData);
if (![computedHashData isEqualToData:hashData]) {
    NSLog(@"validation fails for hash");
    //success=NO;
}
// If an expiration date is present, check it
if (expirationDate) {
    NSDate *currentDate = [NSDate date];
    if ([expirationDate compare:currentDate] == NSOrderedAscending) {
         NSLog(@"volume purchased fails");
        success=NO;
    }
}
if (success){
    [self provideContent:transaction.payment.productIdentifier
          forReceipt:iTunesValidationReceipt
       hostedContent:downloads];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
else [self failedTransaction:transaction];
EVP_cleanup();

}

Has anyone threaded this way or has some suggestion on how to fix it?

Thanks, Fabrizio

fbartolom avatar Dec 30 '14 10:12 fbartolom