An attempt to deal with this SO question
using golang.org/x/crypto/cryptobyte
.
Created
December 10, 2021 18:23
-
-
Save kostix/d71582edd6862df19d17135ea99b20b2 to your computer and use it in GitHub Desktop.
Low-level parsing of an AppStore Receipts DER-encoded block (malformed)
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
module example.com/appstore/receipt | |
go 1.15 | |
require golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b |
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
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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The parsing fails with
and this really happens at the end of input (
xxd
-ed):This is the
300f 0201 0702
sequence: 0x30 is aSEQUENCE
representing another receipt, 0x0f is its length in bytes (ten), 0x02 is anINTEGER
representing the receipt'stype
field, 0x02, 0x07 is that type's value (DER-encoded), 0x02 is anotherINTEGER
of theversion
field, and then there is end of data.