How to decode PKCS#7/ASN.1 using Javascript?

480 Views Asked by At

Recently I started to work on Apple app store receipt validation and due to the deprecation of the legacy /verifyReceipt endpoint, I decided to go for on-device validation. The guideline described gives a step-by-step solution for MacOS however we want to perform this validation in the backend service using NodeJs. For the purpose of this validation information defined in the PCKS7# container is required to be decoded. Here my knowledge comes short as I am unable to retrieve this information (e.g. receipt_creation_data, bundle_id) I managed to convert the receipt from PCKS7 to ASN.1 but could not find a way to retrieve the actual key values from it. I tried several libraries like node-forge, asn1js, asn1.js. What I found really useful were these resources:

AFAIK the information should be encoded in OCTET STRING format enter image description here

How can information such as bundle_id or receipt_creation_date be retrieved from ASN.1 using Javascript?

2

There are 2 best solutions below

0
Juraj Zovinec On BEST ANSWER

With the help of everyone who commented I understood what needed to be done. Here is my very premature implementation of the receipt parser. The key is to define the proper ASN.1 schema that will be used for BER verification. asn1js package showed as very well documented and also well working.

import * as asn1js from "asn1js"

export class ReceiptParser {
  protected readonly PKCS7_DATA_SCHEMA_ID = "PKCS7Data"

  protected readonly FT_TYPE_BASE_ID = "FieldType"

  protected readonly FT_TYPE_OCTET_STRING_ID = "FieldTypeOctetString"

  protected readonly fieldTypesMap: Map<number, string> = new Map([
    [0, "FT_STAGE"],
    [2, "FT_BUNDLE_ID"],
    [3, "FT_APPLICATION_VERSION"],
    [4, "FT_OPAQUE_VALUE"],
    [5, "FT_SHA1_HASH"],
    [12, "FT_RECEIPT_CREATION_DATE"],
    [17, "FT_IN_APP"],
    [18, "FT_ORIGINAL_PURCHASE_DATE"],
    [19, "FT_ORIGINAL_APPLICATION_VERSION"],
    [21, "FT_EXPIRATION_DATE"],
  ])

  protected readonly schema = new asn1js.Sequence({
    value: [
      new asn1js.ObjectIdentifier(),
      new asn1js.Constructed({
        idBlock: {tagClass: 3, tagNumber: 0},
        value: [
          new asn1js.Sequence({
            value: [
              new asn1js.Integer(),
              new asn1js.Set({
                value: [
                  new asn1js.Sequence({
                    value: [new asn1js.ObjectIdentifier(), new asn1js.Any()],
                  }),
                ],
              }),
              new asn1js.Sequence({
                value: [
                  new asn1js.ObjectIdentifier(),
                  new asn1js.Constructed({
                    idBlock: {tagClass: 3, tagNumber: 0},
                    value: [
                      new asn1js.OctetString({name: this.PKCS7_DATA_SCHEMA_ID}),
                    ],
                  }),
                ],
              }),
            ],
          }),
        ],
      }),
    ],
  })

  public parse(receiptString: string) {
    const rootSchemaVerification = asn1js.verifySchema(
      Buffer.from(receiptString, "base64"),
      this.schema
    )

    if (!rootSchemaVerification.verified) {
      throw new Error("Receipt is not valid")
    }

    const pkcs7Data = rootSchemaVerification.result[
      this.PKCS7_DATA_SCHEMA_ID
    ] as asn1js.OctetString

    const fieldTypesSet = pkcs7Data.valueBlock.value[0]

    if (!this.isSet(fieldTypesSet)) {
      throw new Error("Receipt is not valid")
    }

    const resultMap = new Map<string, string>()

    fieldTypesSet.valueBlock.value.forEach((sequence) => {
      const verifiedSequence = asn1js.verifySchema(
        (sequence as asn1js.Sequence).toBER(),
        new asn1js.Sequence({
          value: [
            new asn1js.Integer({name: this.FT_TYPE_BASE_ID}),
            new asn1js.Integer(),
            new asn1js.OctetString({name: this.FT_TYPE_OCTET_STRING_ID}),
          ],
        })
      )

      if (!verifiedSequence.verified) {
        return
      }

      const fieldTypeKey = verifiedSequence.result[
        this.FT_TYPE_BASE_ID
      ] as asn1js.Integer
      const integerValue = fieldTypeKey.valueBlock.valueDec

      if (!this.fieldTypesMap.has(integerValue)) {
        return
      }

      const octetValueId = this.FT_TYPE_OCTET_STRING_ID
      const octetValue = verifiedSequence.result[
        octetValueId
      ] as asn1js.OctetString

      const valueEncodedWithinOctetString = octetValue.valueBlock.value[0]

      if (valueEncodedWithinOctetString instanceof asn1js.IA5String) {
        const result = valueEncodedWithinOctetString.valueBlock.value
        resultMap.set(this.fieldTypesMap.get(integerValue)!, result)
      }

      if (valueEncodedWithinOctetString instanceof asn1js.Utf8String) {
        const result = valueEncodedWithinOctetString.valueBlock.value
        resultMap.set(this.fieldTypesMap.get(integerValue)!, result)
      }
    })

    return resultMap.get("FT_BUNDLE_ID")
  }

  protected isConstructed(data: unknown): data is asn1js.Constructed {
    return Boolean(data && data instanceof asn1js.Constructed)
  }

  protected isSequence(data: unknown): data is asn1js.Sequence {
    return Boolean(data && data instanceof asn1js.Sequence)
  }

  protected isSet(data: unknown): data is asn1js.Set {
    return Boolean(data && data instanceof asn1js.Set)
  }

  protected isInteger(data: unknown): data is asn1js.Integer {
    return Boolean(data && data instanceof asn1js.Integer)
  }
}
0
Kolzar On

// Base64 string of ASN.1 or PKCS#7 data
const base64Data = '...'; // Replace with your Base64 data

// Decode Base64
const binaryData = atob(base64Data);

// Transform the binary string into a byte array
const byteArray = binaryData.split('').map(char => char.charCodeAt(0));

// Now you have the `byteArray` containing the bytes of the binary data

// At this point, you would manually parse the ASN.1 or PKCS#7 structure.
// This involves reading and interpreting headers, tags, and data within the byte array.
// This operation is highly complex and requires a deep understanding of ASN.1 and PKCS#7 formats.

// For example, with ASN.1, you might read bytes, interpret length and field tags, extract data, etc.

// For PKCS#7, you would need to understand the PKCS#7 message structure and read the data accordingly.

// It's important to note that this method requires an in-depth understanding of ASN.1 and PKCS#7 specifications, in addition to being subject to many complexities and potential errors.

However, this is merely a basic outline, and actual decoding would involve much more manual work to properly interpret the structure of ASN.1 or PKCS#7 data, handle defined lengths, tags, sequences, and so on. Using specialized libraries such as npm install node-forge or npm install asn1js significantly simplifies this process by providing more understandable and reliable ASN.1 parsing functionalities.