Last active
March 17, 2018 18:19
-
-
Save lenew/d03cee2be37ed959463646a78f7bfd47 to your computer and use it in GitHub Desktop.
Apple IAP Receipt Extract Tool
This file contains hidden or 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
/* | |
* A Http Server to verify an Apple IAP Receipt | |
* Return with Receipts data | |
* Usage Example: curl -X POST http://127.0.0.1:5000/verifyReceipt -d '{"receipt-data":"apple receipt base64 data"}' | |
*/ | |
package main | |
import ( | |
"crypto/rsa" | |
"crypto/x509" | |
"encoding/asn1" | |
"encoding/json" | |
"encoding/pem" | |
"fmt" | |
"net/http" | |
"strings" | |
"github.com/fullsailor/pkcs7" | |
) | |
type PemResult struct { | |
Input []byte | |
Certificate *x509.Certificate | |
PrivateKey *rsa.PrivateKey | |
} | |
func UnmarshalPemStr(testPEMBlock string) PemResult { | |
var result PemResult | |
var derBlock *pem.Block | |
var pemBlock = []byte(testPEMBlock) | |
for { | |
derBlock, pemBlock = pem.Decode(pemBlock) | |
if derBlock == nil { | |
break | |
} | |
switch derBlock.Type { | |
case "PKCS7": | |
result.Input = derBlock.Bytes | |
case "CERTIFICATE": | |
result.Certificate, _ = x509.ParseCertificate(derBlock.Bytes) | |
case "PRIVATE KEY": | |
result.PrivateKey, _ = x509.ParsePKCS1PrivateKey(derBlock.Bytes) | |
} | |
} | |
return result | |
} | |
type IAPAttribute struct { | |
Type int | |
Version int | |
Value []byte | |
} | |
type PayloadObject struct { | |
ApplicationVersion string | |
ReceiptType string | |
BundleId string | |
OriginalApplicationVersion string | |
CreationDate string | |
ExpirationDate string | |
Hash []byte | |
Receipts []ReceiptObject | |
} | |
type ReceiptObject struct { | |
Quantity int | |
ProductId string | |
TransactionId string | |
PurchaseDate string | |
OriginalPurchaseDate string | |
ExpiresDate string | |
OriginalTransactionId string | |
ExpireDate string | |
WebOrderLineItemId int | |
CancellationDate string | |
IsInIntroOfferPeriod int | |
} | |
type VerifyRequest struct { | |
Payload string `json:"receipt-data"` | |
} | |
func loadAllReceipts(data []byte) ReceiptObject { | |
var receipts []IAPAttribute | |
asn1.UnmarshalWithParams(data, &receipts, "set") | |
var receiptObject ReceiptObject | |
for _, receipt := range receipts { | |
if receipt.Type == 1701 { | |
var val int | |
asn1.Unmarshal(receipt.Value, &val) | |
receiptObject.Quantity = val | |
} else if receipt.Type == 1702 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.ProductId = str | |
} else if receipt.Type == 1703 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.TransactionId = str | |
} else if receipt.Type == 1704 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.PurchaseDate = str | |
} else if receipt.Type == 1705 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.OriginalTransactionId = str | |
} else if receipt.Type == 1706 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.OriginalPurchaseDate = str | |
} else if receipt.Type == 1708 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.ExpiresDate = str | |
} else if receipt.Type == 1711 { | |
var val int | |
asn1.Unmarshal(receipt.Value, &val) | |
receiptObject.WebOrderLineItemId = val | |
} else if receipt.Type == 1719 { | |
var val int | |
asn1.Unmarshal(receipt.Value, &val) | |
receiptObject.IsInIntroOfferPeriod = val | |
} else if receipt.Type == 1712 { | |
var str string | |
asn1.Unmarshal(receipt.Value, &str) | |
receiptObject.CancellationDate = str | |
} | |
} | |
return receiptObject | |
} | |
func decodePayload(data []byte) PayloadObject { | |
var payloads2 []IAPAttribute | |
asn1.UnmarshalWithParams(data, &payloads2, "set") | |
var payloadObject PayloadObject | |
for _, el := range payloads2 { | |
if el.Type == 0 { | |
var str string | |
asn1.Unmarshal(el.Value, &str) | |
payloadObject.ReceiptType = str | |
} else if el.Type == 2 { | |
var str string | |
asn1.Unmarshal(el.Value, &str) | |
payloadObject.BundleId = str | |
} else if el.Type == 3 { | |
var str string | |
asn1.Unmarshal(el.Value, &str) | |
payloadObject.ApplicationVersion = str | |
} else if el.Type == 4 { | |
} else if el.Type == 5 { | |
payloadObject.Hash = el.Value | |
} else if el.Type == 17 { | |
receipt := loadAllReceipts(el.Value) | |
payloadObject.Receipts = append(payloadObject.Receipts, receipt) | |
} else if el.Type == 19 { | |
var str string | |
asn1.Unmarshal(el.Value, &str) | |
payloadObject.OriginalApplicationVersion = str | |
} else if el.Type == 12 { | |
var str string | |
asn1.Unmarshal(el.Value, &str) | |
payloadObject.CreationDate = str | |
} else if el.Type == 21 { | |
var str string | |
asn1.Unmarshal(el.Value, &str) | |
payloadObject.ExpirationDate = str | |
} | |
} | |
return payloadObject | |
} | |
func verifyCertChain(roots *x509.CertPool, p7 *pkcs7.PKCS7) bool { | |
opts := x509.VerifyOptions{ | |
Roots: roots, | |
Intermediates: x509.NewCertPool(), | |
} | |
ok := true | |
for i := len(p7.Certificates) - 1; i >= 0; i-- { | |
cert := p7.Certificates[i] | |
if _, err := cert.Verify(opts); err != nil { | |
ok = false | |
fmt.Println(ok) | |
return ok | |
} | |
if i>0 { | |
opts.Intermediates.AddCert(cert) | |
} | |
} | |
return ok | |
} | |
func main() { | |
roots := x509.NewCertPool() | |
roots.AppendCertsFromPEM([]byte(AppleCA)) | |
http.HandleFunc("/verifyReceipt", func(w http.ResponseWriter, r *http.Request) { | |
if r.Method != "POST" { | |
w.WriteHeader(http.StatusNotFound) | |
return | |
} | |
var ur VerifyRequest | |
decoder := json.NewDecoder(r.Body) | |
decoder.Decode(&ur) | |
if strings.Count(ur.Payload, "") > 1 { | |
fixture := UnmarshalPemStr("-----BEGIN PKCS7-----\n" + ur.Payload + "\n-----END PKCS7-----") | |
p7, err := pkcs7.Parse(fixture.Input) | |
if err != nil { | |
fmt.Fprintf(w, `{"error":"Parse encountered unexpected error: %v"}`, err) | |
} | |
if err := p7.Verify(); err != nil { | |
fmt.Fprintf(w, `{"error":"Verify failed with error: %v"}`, err) | |
return | |
} | |
ok := verifyCertChain(roots, p7) | |
if !ok { | |
fmt.Fprintf(w, `{"error":"Verify certificates error"}`) | |
return | |
} | |
payloadObject := decodePayload(p7.Content) | |
b, _ := json.Marshal(payloadObject) | |
w.Header().Set("Content-Type", "application/json") | |
fmt.Fprintf(w, string(b)) | |
} | |
}) | |
http.ListenAndServe(":5000", nil) | |
fmt.Println("server started") | |
} | |
var AppleCA = ` | |
-----BEGIN CERTIFICATE----- | |
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET | |
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv | |
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 | |
MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw | |
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx | |
FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw | |
ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ | |
+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 | |
XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w | |
tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW | |
q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM | |
aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E | |
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 | |
R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE | |
ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 | |
d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl | |
IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 | |
YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj | |
b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp | |
Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc | |
NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP | |
y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 | |
R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg | |
xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP | |
IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX | |
UKqK1drk/NAJBzewdXUh | |
-----END CERTIFICATE----- | |
` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment