Question

I want to validate iOS in-app purchase receipts in my backend code.

Apple's design decision to do this using an external /verifyReceipt request is plain down stupid: it incurs latency and adds complexity of network error handling. Even more so, the data in the receipt looks like it can be public key verified.

After a bit of analysis on the signature field of a receipt, it seems to contain a PK-verified SHA1 hash:

<?php
$sig="ApxQMks+KAE0riYtKjNNwhNeuGQ6R98X223zCh60s9m8wloydP3sCceQdzrCwd/3N1L+dlefT7ZJUiquCEsDAo+Rh54eSovcKEk+2RZyoP/zRQHgTF81kYBIbkFCADhj6kzJVr1rYsRXKpOJk6qWMYPz+a90XJfGtnIDuHlRb4V5AAADVzCCA1MwggI7oAMCAQICCGUUkU3ZWAS1MA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA5MDYxNTIyMDU1NloXDTE0MDYxNDIyMDU1NlowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMrRjF2ct4IrSdiTChaI0g8pwv/cmHs8p/RwV/rt/91XKVhNl4XIBimKjQQNfgHsDs6yju++DrKJE7uKsphMddKYfFE5rGXsAdBEjBwRIxexTevx3HLEFGAt1moKx509dhxtiIdDgJv2YaVs49B0uJvNdy6SMqNNLHsDLzDS9oZHAgMBAAGjcjBwMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUNh3o4p2C0gEYtTJrDtdDC5FYQzowDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBSpg4PyGUjFPhJXCBTMzaN+mV8k9TAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAEaSbPjtmN4C/IB3QEpK32RxacCDXdVXAeVReS5FaZxc+t88pQP93BiAxvdW/3eTSMGY5FbeAYL3etqP5gm8wrFojX0ikyVRStQ+/AQ0KEjtqB07kLs9QUe8czR8UGfdM1EumV/UgvDd4NwNYxLQMg4WTQfgkQQVy8GXZwVHgbE/UC6Y7053pGXBk51NPM3woxhd3gSRLvXj+loHsStcTEqe9pBDpmG5+sk4tw+GK3GMeEN5/+e1QT9np/Kl1nj+aBw7C0xsy0bFnaAd1cSS6xdory/CUvM6gtKsmnOOdqTesbp0bs8sn6Wqs0C9dgcxRHuOMZ2tm8npLUm7argOSzQ==";

file_put_contents('sig', substr(base64_decode($sig),1,128));
file_put_contents('cert.der', substr(base64_decode($sig),133));

# show certificate
echo `openssl x509 -in cert.der -inform der -noout -text` . "\n\n";

# convert to pem
`openssl x509 -in cert.der -inform der -out cert.pem`;

echo "signature:\n";
echo `openssl rsautl -in sig -verify -asn1parse -inkey cert.pem -certin`;
echo "\n\n";

Output:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            65:14:91:4d:d9:58:04:b5
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority,     CN=Apple iTunes Store Certification Authority
        Validity
            Not Before: Jun 15 22:05:56 2009 GMT
            Not After : Jun 14 22:05:56 2014 GMT
        Subject: CN=PurchaseReceiptCertificate, OU=Apple iTunes Store,     O=Apple Inc., C=US
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (1024 bit)
                Modulus (1024 bit):
                    00:ca:d1:8c:5d:9c:b7:82:2b:49:d8:93:0a:16:88:
                    d2:0f:29:c2:ff:dc:98:7b:3c:a7:f4:70:57:fa:ed:
                    ff:dd:57:29:58:4d:97:85:c8:06:29:8a:8d:04:0d:
                    7e:01:ec:0e:ce:b2:8e:ef:be:0e:b2:89:13:bb:8a:
                    b2:98:4c:75:d2:98:7c:51:39:ac:65:ec:01:d0:44:
                    8c:1c:11:23:17:b1:4d:eb:f1:dc:72:c4:14:60:2d:
                    d6:6a:0a:c7:9d:3d:76:1c:6d:88:87:43:80:9b:f6:
                    61:a5:6c:e3:d0:74:b8:9b:cd:77:2e:92:32:a3:4d:
                    2c:7b:03:2f:30:d2:f6:86:47
                Exponent: 65537 (0x10001)
     --- cut ---

signature:
    0:d=0  hl=2 l=  33 cons: SEQUENCE          
    2:d=1  hl=2 l=   9 cons:  SEQUENCE          
    4:d=2  hl=2 l=   5 prim:   OBJECT            :sha1
   11:d=2  hl=2 l=   0 prim:   NULL              
   13:d=1  hl=2 l=  20 prim:  OCTET STRING      
      0000 - b7 ef f1 9e 01 2a dd 26-09 38 cd ce 63 5b b1 32   .....*.&.8..c[.2
      0010 - 88 51 17 0a                                       .Q..  

The question now remains of which data this is a hash. The above contains an actual signature of this receipt:

{
        "signature" = "ApxQMks+KAE0riYtKjNNwhNeuGQ6R98X223zCh60s9m8wloydP3sCceQdzrCwd/3N1L+dlefT7ZJUiquCEsDAo+Rh54eSovcKEk+2RZyoP/zRQHgTF81kYBIbkFCADhj6kzJVr1rYsRXKpOJk6qWMYPz+a90XJfGtnIDuHlRb4V5AAADVzCCA1MwggI7oAMCAQICCGUUkU3ZWAS1MA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA5MDYxNTIyMDU1NloXDTE0MDYxNDIyMDU1NlowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMrRjF2ct4IrSdiTChaI0g8pwv/cmHs8p/RwV/rt/91XKVhNl4XIBimKjQQNfgHsDs6yju++DrKJE7uKsphMddKYfFE5rGXsAdBEjBwRIxexTevx3HLEFGAt1moKx509dhxtiIdDgJv2YaVs49B0uJvNdy6SMqNNLHsDLzDS9oZHAgMBAAGjcjBwMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUNh3o4p2C0gEYtTJrDtdDC5FYQzowDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBSpg4PyGUjFPhJXCBTMzaN+mV8k9TAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAEaSbPjtmN4C/IB3QEpK32RxacCDXdVXAeVReS5FaZxc+t88pQP93BiAxvdW/3eTSMGY5FbeAYL3etqP5gm8wrFojX0ikyVRStQ+/AQ0KEjtqB07kLs9QUe8czR8UGfdM1EumV/UgvDd4NwNYxLQMg4WTQfgkQQVy8GXZwVHgbE/UC6Y7053pGXBk51NPM3woxhd3gSRLvXj+loHsStcTEqe9pBDpmG5+sk4tw+GK3GMeEN5/+e1QT9np/Kl1nj+aBw7C0xsy0bFnaAd1cSS6xdory/CUvM6gtKsmnOOdqTesbp0bs8sn6Wqs0C9dgcxRHuOMZ2tm8npLUm7argOSzQ==";
        "purchase-info" = "ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE0LTAxLTI3IDA0OjUwOjU2IEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInB1cmNoYXNlLWRhdGUtbXMiID0gIjEzOTA4MjcwNTYxNzciOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICJiMTUxOTczZThjYzY3ZGRlMzkwNzhiZDAwMGU4N2U3MjNiYjE0M2U1IjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDA5OTcwODE1MCI7CgkiYnZycyIgPSAiMS4wLjciOwoJImFwcC1pdGVtLWlkIiA9ICI3MTE0MTEzMTciOwoJInRyYW5zYWN0aW9uLWlkIiA9ICIxMDAwMDAwMDk5NzA4MTUwIjsKCSJxdWFudGl0eSIgPSAiMSI7Cgkib3JpZ2luYWwtcHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTM5MDgyNzA1NjE3NyI7CgkidW5pcXVlLXZlbmRvci1pZGVudGlmaWVyIiA9ICJCNjNBRTVFQy02MjBCLTQxMkEtQjE5NC03NUI3MDU3Mjk4M0MiOwoJIml0ZW0taWQiID0gIjc3NTg5MTg5MiI7CgkidmVyc2lvbi1leHRlcm5hbC1pZGVudGlmaWVyIiA9ICIxNzU5NTMxMzgiOwoJInByb2R1Y3QtaWQiID0gImNvbS5pbnRvbXlsaWZlLmNyZWRpdHMyNSI7CgkicHVyY2hhc2UtZGF0ZSIgPSAiMjAxNC0wMS0yNyAxMjo1MDo1NiBFdGMvR01UIjsKCSJvcmlnaW5hbC1wdXJjaGFzZS1kYXRlIiA9ICIyMDE0LTAxLTI3IDEyOjUwOjU2IEV0Yy9HTVQiOwoJImJpZCIgPSAiY29tLmludG9teWxpZmUuaW9zIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNC0wMS0yNyAwNDo1MDo1NiBBbWVyaWNhL0xvc19BbmdlbGVzIjsKfQ==";
        "environment" = "Sandbox";
        "pod" = "100";
        "signing-status" = "0";
}

Or, in base64:

$receipt="ewoJInNpZ25hdHVyZSIgPSAiQXB4UU1rcytLQUUwcmlZdEtqTk53aE5ldUdRNlI5OFgyMjN6Q2g2MHM5bTh3bG95ZFAzc0NjZVFkenJDd2QvM04xTCtkbGVmVDdaSlVpcXVDRXNEQW8rUmg1NGVTb3ZjS0VrKzJSWnlvUC96UlFIZ1RGODFrWUJJYmtGQ0FEaGo2a3pKVnIxcllzUlhLcE9KazZxV01ZUHorYTkwWEpmR3RuSUR1SGxSYjRWNUFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5USXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1YvcnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRkS1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNqQndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBnRVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERnUVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lCM1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNlVFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIwN2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FRVnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xvSHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5qK2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2JwMGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUwTFRBeExUSTNJREEwT2pVd09qVTJJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkluQjFjbU5vWVhObExXUmhkR1V0YlhNaUlEMGdJakV6T1RBNE1qY3dOVFl4TnpjaU93b0pJblZ1YVhGMVpTMXBaR1Z1ZEdsbWFXVnlJaUE5SUNKaU1UVXhPVGN6WlRoall6WTNaR1JsTXprd056aGlaREF3TUdVNE4yVTNNak5pWWpFME0yVTFJanNLQ1NKdmNtbG5hVzVoYkMxMGNtRnVjMkZqZEdsdmJpMXBaQ0lnUFNBaU1UQXdNREF3TURBNU9UY3dPREUxTUNJN0Nna2lZblp5Y3lJZ1BTQWlNUzR3TGpjaU93b0pJbUZ3Y0MxcGRHVnRMV2xrSWlBOUlDSTNNVEUwTVRFek1UY2lPd29KSW5SeVlXNXpZV04wYVc5dUxXbGtJaUE5SUNJeE1EQXdNREF3TURrNU56QTRNVFV3SWpzS0NTSnhkV0Z1ZEdsMGVTSWdQU0FpTVNJN0Nna2liM0pwWjJsdVlXd3RjSFZ5WTJoaGMyVXRaR0YwWlMxdGN5SWdQU0FpTVRNNU1EZ3lOekExTmpFM055STdDZ2tpZFc1cGNYVmxMWFpsYm1SdmNpMXBaR1Z1ZEdsbWFXVnlJaUE5SUNKQ05qTkJSVFZGUXkwMk1qQkNMVFF4TWtFdFFqRTVOQzAzTlVJM01EVTNNams0TTBNaU93b0pJbWwwWlcwdGFXUWlJRDBnSWpjM05UZzVNVGc1TWlJN0Nna2lkbVZ5YzJsdmJpMWxlSFJsY201aGJDMXBaR1Z1ZEdsbWFXVnlJaUE5SUNJeE56VTVOVE14TXpnaU93b0pJbkJ5YjJSMVkzUXRhV1FpSUQwZ0ltTnZiUzVwYm5SdmJYbHNhV1psTG1OeVpXUnBkSE15TlNJN0Nna2ljSFZ5WTJoaGMyVXRaR0YwWlNJZ1BTQWlNakF4TkMwd01TMHlOeUF4TWpvMU1EbzFOaUJGZEdNdlIwMVVJanNLQ1NKdmNtbG5hVzVoYkMxd2RYSmphR0Z6WlMxa1lYUmxJaUE5SUNJeU1ERTBMVEF4TFRJM0lERXlPalV3T2pVMklFVjBZeTlIVFZRaU93b0pJbUpwWkNJZ1BTQWlZMjl0TG1sdWRHOXRlV3hwWm1VdWFXOXpJanNLQ1NKd2RYSmphR0Z6WlMxa1lYUmxMWEJ6ZENJZ1BTQWlNakF4TkMwd01TMHlOeUF3TkRvMU1EbzFOaUJCYldWeWFXTmhMMHh2YzE5QmJtZGxiR1Z6SWpzS2ZRPT0iOwoJImVudmlyb25tZW50IiA9ICJTYW5kYm94IjsKCSJwb2QiID0gIjEwMCI7Cgkic2lnbmluZy1zdGF0dXMiID0gIjAiOwp9";

The most simple data to verify would be the purchase-info field. Unfortunately, the sha1 sum of this (either base64-encoded or opaque) data does not match the one in the signature.

Having seen a fair share of Apple file formats, my guess would be it could be a combination in a form like "{$purchaseData}\x00\x00{$environment}\x00\x00{$pod}". With a bit of bad luck, though, they added a secret string that would make the entire exercise quite futile (but I fail to see as to why they would...)

Any insight?

update

Some more experimentation with sending different receipts to the /verifyReceipt endpoint suggests the pod/environment fields do not matter. Even more so, the order of fields within the receipt structure is of no consequence. Changing a single byte in the purchase-info data, however, directly yields an invalid receipt. This all would strengthen the hypothesis that only the purchase-info value is part of the hash -- but it's probably prefixed/suffixed with a secret. Could anyone verify (pun intended) this?

Was it helpful?

Solution

Please look at the code in https://developer.apple.com/library/ios/releasenotes/StoreKit/IAP_ReceiptValidation/index.html

The signature structure is

#pragma pack(push, 1)
struct signature_blob {
    uint8_t version;
    uint8_t signature[128];
    uint32_t cert_len;
    uint8_t certificate[];
} *signature_blob_ptr = (struct signature_blob *)signature_bytes;
#pragma pack(pop)

to_be_hashed= signature_blob_ptr->version . base64_decode(purchase_info)

code here

<?php
$plist = file_get_contents($argv[1]);
if(preg_match('/"signature" = "([^"]+)"/',$plist,$s) && preg_match('/"purchase-info" = "([^"]+)"/',$plist,$p)){
    $sig= base64_decode($s[1]);
    $purchase=base64_decode($p[1]);

    $version=substr($sig,0,1);
    $cert = substr($sig,133);
    $sig = substr($sig,1,128);
    $to_be_sign=$version.$purchase;
    //get PEM encode
    $cert="-----BEGIN CERTIFICATE-----\n".wordwrap(base64_encode($cert),64,"\n",true)."\n-----END CERTIFICATE-----";
    $pubkey = openssl_get_publickey($cert);
    if(openssl_verify($to_be_sign,$sig,$pubkey) ==1 )
            echo "Success\n";
    else
            echo "Failed\n";
    openssl_free_key($pubkey);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top