Skip to content

Instantly share code, notes, and snippets.

@kostix
Created December 10, 2021 18:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kostix/d71582edd6862df19d17135ea99b20b2 to your computer and use it in GitHub Desktop.
Save kostix/d71582edd6862df19d17135ea99b20b2 to your computer and use it in GitHub Desktop.
Low-level parsing of an AppStore Receipts DER-encoded block (malformed)
module example.com/appstore/receipt
go 1.15
require golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b
package main
import (
"encoding/base64"
"fmt"
"log"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)
const b64Data = `MYIEMzA5AgECAgEBBDE2TVVSTDhUQTU3LmRlLnZpbmNlbnQtaGF1cGVydC5hcHBsZS1hcHBhdHRl
c3QtcG9jMIIDAwIBAwIBAQSCAvkwggL1MIICe6ADAgECAgYBdy8p90gwCgYIKoZIzj0EAwIwTzEj
MCEGA1UEAwwaQXBwbGUgQXBwIEF0dGVzdGF0aW9uIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4x
EzARBgNVBAgMCkNhbGlmb3JuaWEwHhcNMjEwMTIyMTIxMzM1WhcNMjEwMTI1MTIxMzM1WjCBkTFJ
MEcGA1UEAwxANjI2NmM5M2I4Yzc5OWM0MWQ0YmU3NzI5ZjczNzU2Yjk1NjYzMzQxMTBjODA5OWY3
NzFkNDkzYTAwNWQwN2I3MzEaMBgGA1UECwwRQUFBIENlcnRpZmljYXRpb24xEzARBgNVBAoMCkFw
cGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASI
wDShkKp9vFoGFQHGVCgDlCWCGYs/HMVGc8o7mtILQVKCZ6VPX9ugRp+vtGu2mQo5a/BPlKSdQyDI
HHqyQKOYo4H/MIH8MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgTwMIGLBgkqhkiG92NkCAUE
fjB8pAMCAQq/iTADAgEBv4kxAwIBAL+JMgMCAQC/iTMDAgEBv4k0MwQxNk1VUkw4VEE1Ny5kZS52
aW5jZW50LWhhdXBlcnQuYXBwbGUtYXBwYXR0ZXN0LXBvY6UGBAQgc2tzv4k2AwIBBb+JNwMCAQC/
iTkDAgEAv4k6AwIBADAZBgkqhkiG92NkCAcEDDAKv4p4BgQEMTQuNDAzBgkqhkiG92NkCAIEJjAk
oSIEIJiaPSUYoXwlnO9VFNRc5s3SNR9j2KApAZckQkeewkQ+MAoGCCqGSM49BAMCA2gAMGUCMGgT
pXoTOAhh7XJ4V/W7dVrodoibZVQLQ2+z4d3D0cTnlqYe7se50V/rBM5FSBEMwAIxAN04wxPlelL+
QyuFR3qk8ZZ4CsZTHFyzSlE8a0Kd4zvYnS8+taIoED9GwrUi96TmgDAoAgEEAgEBBCCL5lzKUVrQ
l8lTln0Y1jXYbdeKFC7T0HdSa+0Rxr7GezBgAgEFAgEBBFhhUDVTOVVmeTA5MmNLbGFSWWtrdVR2
UUFUeC9SM0I5U3dxSHI2SzZGWGFBV3N6clQrMnhrQWdLTUVmbDI2UFhacG5WWWFZejNySmkzZElB
cUVaZXViUT09MA4CAQYCAQEEBkFUVEVTVDAPAgEHAg==`
func main() {
log.SetFlags(0)
bin, err := base64.StdEncoding.DecodeString(b64Data)
if err != nil {
log.Fatalf("failed to base64-decode data: %s", err)
}
input := cryptobyte.String(bin)
if !skipConstructedTag(&input, asn1.SET) {
log.Fatal("failed to skip top-level SET")
}
var rcpts []receipt
for !atEnd(input) {
var rcpt receipt
if !readReceipt(&input, &rcpt) {
log.Fatal("failed to read receipt")
}
fmt.Printf("%#v\n", rcpt)
rcpts = append(rcpts, rcpt)
}
}
func atEnd(s cryptobyte.String) bool {
return len(s) == 0
}
type receipt struct {
receiptType int
version int
value []byte
}
func readReceipt(s *cryptobyte.String, out *receipt) bool {
if !skipConstructedTag(s, asn1.SEQUENCE) {
log.Fatal("failed to skip top-level SEQUENCE")
}
if !s.ReadASN1Integer(&out.receiptType) {
log.Fatal("failed to read type")
}
if !s.ReadASN1Integer(&out.version) {
log.Fatal("failed to read version")
}
if !s.ReadASN1Bytes(&out.value, asn1.OCTET_STRING) {
log.Fatal("failed to read value")
}
return true
}
func skipConstructedTag(s *cryptobyte.String, tag asn1.Tag) bool {
if len(*s) < 2 {
return false
}
_tag, lenByte := (*s)[0], (*s)[1]
if asn1.Tag(_tag) != tag {
return false
}
// ITU-T X.690 section 8.1.3
//
// Bit 8 of the first length byte indicates whether the length is short- or
// long-form.
var length, headerLen uint32 // length includes headerLen
if lenByte&0x80 == 0 {
// Short-form length (section 8.1.3.4), encoded in bits 1-7.
length = uint32(lenByte) + 2
headerLen = 2
} else {
// Long-form length (section 8.1.3.5). Bits 1-7 encode the number of octets
// used to encode the length.
lenLen := lenByte & 0x7f
var len32 uint32
if lenLen == 0 || lenLen > 4 || len(*s) < int(2+lenLen) {
return false
}
lenBytes := cryptobyte.String((*s)[2 : 2+lenLen])
if !readUnsigned(&lenBytes, &len32, int(lenLen)) {
return false
}
// ITU-T X.690 section 10.1 (DER length forms) requires encoding the length
// with the minimum number of octets.
if len32 < 128 {
// Length should have used short-form encoding.
return false
}
if len32>>((lenLen-1)*8) == 0 {
// Leading octet is 0. Length should have been at least one byte shorter.
return false
}
headerLen = 2 + uint32(lenLen)
if headerLen+len32 < len32 {
// Overflow.
return false
}
length = headerLen + len32
}
if int(length) < 0 || !s.Skip(int(headerLen)) {
return false
}
return true
}
func readUnsigned(s *cryptobyte.String, out *uint32, length int) bool {
var v []byte
if !s.ReadBytes(&v, length) {
return false
}
var result uint32
for i := 0; i < length; i++ {
result <<= 8
result |= uint32(v[i])
}
*out = result
return true
}
@kostix
Copy link
Author

kostix commented Dec 10, 2021

The parsing fails with

failed to read version

and this really happens at the end of input (xxd-ed):

...
000003b0: 4b4d 4566 6c32 3650 585a 706e 5659 6159  KMEfl26PXZpnVYaY
000003c0: 7a33 724a 6933 6449 4171 455a 6575 6251  z3rJi3dIAqEZeubQ
000003d0: 3d3d 300e 0201 0602 0101 0406 4154 5445  ==0.........ATTE
000003e0: 5354 300f 0201 0702                      ST0.....

This is the 300f 0201 0702 sequence: 0x30 is a SEQUENCE representing another receipt, 0x0f is its length in bytes (ten), 0x02 is an INTEGER representing the receipt's type field, 0x02, 0x07 is that type's value (DER-encoded), 0x02 is another INTEGER of the version field, and then there is end of data.

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