-
-
Save ivodvb/b7d2b460720b2f15445d3ac47df5efad to your computer and use it in GitHub Desktop.
Verify an Apple receipt locally by decrypting it, verifying the signatures and pprinting the receipt contents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Tested with Python 3.6 | |
# receipt_data.bin is assumed to be a base64 encoded receipt | |
# pip install asn1crypto cryptography cryptography pprint | |
from pprint import pprint | |
import base64 | |
import urllib.request | |
from asn1crypto.cms import ContentInfo | |
from OpenSSL.crypto import load_certificate, FILETYPE_ASN1, FILETYPE_PEM, X509Store, X509StoreContext, X509StoreContextError, verify | |
from asn1crypto.core import Any, Integer, ObjectIdentifier, OctetString, Sequence, SetOf, UTF8String, IA5String | |
# Load the contents of the receipt file | |
receipt_file = open('./receipt_data.bin', 'rb').read() | |
receipt_file_raw = base64.b64decode(receipt_file) | |
# Use asn1crypto's cms definitions to parse the PKCS#7 format | |
pkcs_container = ContentInfo.load(receipt_file_raw) | |
# Extract the certificates, signature, and receipt_data | |
certificates = pkcs_container['content']['certificates'] | |
signer_info = pkcs_container['content']['signer_infos'][0] | |
receipt_data = pkcs_container['content']['encap_content_info']['content'] | |
# Pull out and parse the X.509 certificates included in the receipt | |
itunes_cert_data = certificates[0].chosen.dump() | |
itunes_cert = load_certificate(FILETYPE_ASN1, itunes_cert_data) | |
itunes_cert_signature = certificates[0].chosen.signature | |
wwdr_cert_data = certificates[1].chosen.dump() | |
wwdr_cert = load_certificate(FILETYPE_ASN1, wwdr_cert_data) | |
wwdr_cert_signature = certificates[1].chosen.signature | |
untrusted_root_data = certificates[2].chosen.dump() | |
untrusted_root = load_certificate(FILETYPE_ASN1, untrusted_root_data) | |
untrusted_root_signature = certificates[2].chosen.signature | |
trusted_root_data = urllib.request.urlopen("https://www.apple.com/appleca/AppleIncRootCertificate.cer").read() | |
trusted_root = load_certificate(FILETYPE_ASN1, trusted_root_data) | |
trusted_store = X509Store() | |
trusted_store.add_cert(trusted_root) | |
try: | |
X509StoreContext(trusted_store, wwdr_cert).verify_certificate() | |
trusted_store.add_cert(wwdr_cert) | |
except X509StoreContextError as e: | |
print("WWDR certificate invalid") | |
exit() | |
try: | |
X509StoreContext(trusted_store, itunes_cert).verify_certificate() | |
except X509StoreContextError as e: | |
print("iTunes certificate invalid") | |
exit() | |
try: | |
verify(itunes_cert, signer_info['signature'].native, receipt_data.native, 'sha1') | |
print("The receipt data signature is valid.") | |
except Exception as e: | |
print("The receipt data is invalid: %s" % e) | |
exit() | |
attribute_types = [ | |
(2, 'bundle_id', UTF8String), | |
(3, 'application_version', UTF8String) , | |
(4, 'opaque_value', None), | |
(5, 'sha1_hash', None), | |
(12, 'creation_date', IA5String), | |
(17, 'in_app', OctetString), | |
(19, 'original_application_version', UTF8String), | |
(21, 'expiration_date', IA5String) | |
] | |
class ReceiptAttributeType(Integer): | |
_map = {type_code: name for type_code, name, _ in attribute_types} | |
class ReceiptAttribute(Sequence): | |
_fields = [ | |
('type', ReceiptAttributeType), | |
('version', Integer), | |
('value', OctetString) | |
] | |
class Receipt(SetOf): | |
_child_spec = ReceiptAttribute | |
receipt = Receipt.load(receipt_data.native) | |
receipt_attributes = {} | |
attribute_types_to_class = {name: type_class for _, name, type_class in attribute_types} | |
in_apps = [] | |
for attr in receipt: | |
attr_type = attr['type'].native | |
# Just store the in_apps for now | |
if attr_type == 'in_app': | |
in_apps.append(attr['value']) | |
continue | |
if attr_type in attribute_types_to_class: | |
if attribute_types_to_class[attr_type] is not None: | |
receipt_attributes[attr_type] = attribute_types_to_class[attr_type].load(attr['value'].native).native | |
else: | |
receipt_attributes[attr_type] = attr['value'].native | |
in_app_attribute_types = { | |
(1701, 'quantity', Integer), | |
(1702, 'product_id', UTF8String), | |
(1703, 'transaction_id', UTF8String), | |
(1705, 'original_transaction_id', UTF8String), | |
(1704, 'purchase_date', IA5String), | |
(1706, 'original_purchase_date', IA5String), | |
(1708, 'expires_date', IA5String), | |
(1719, 'is_in_intro_offer_period', Integer), | |
(1712, 'cancellation_date', IA5String), | |
(1711, 'web_order_line_item_id', Integer) | |
} | |
class InAppAttributeType(Integer): | |
_map = {type_code: name for (type_code, name, _) in in_app_attribute_types} | |
class InAppAttribute(Sequence): | |
_fields = [ | |
('type', InAppAttributeType), | |
('version', Integer), | |
('value', OctetString) | |
] | |
class InAppPayload(SetOf): | |
_child_spec = InAppAttribute | |
in_app_attribute_types_to_class = {name: type_class for _, name, type_class in in_app_attribute_types} | |
in_apps_parsed = [] | |
for in_app_data in in_apps: | |
in_app = {} | |
for attr in InAppPayload.load(in_app_data.native): | |
attr_type = attr['type'].native | |
if attr_type in in_app_attribute_types_to_class: | |
in_app[attr_type] = in_app_attribute_types_to_class[attr_type].load(attr['value'].native).native | |
in_apps_parsed.append(in_app) | |
receipt_attributes['in_app'] = in_apps_parsed | |
pprint(receipt_attributes) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment