Decoding and parsing App Attest receipts

I'm using App Attest and the endpoint mentioned here to receive receipts with a fraud metric from Apple on my server. However, I've so far been unable to decode the receipts sent by Apple's server. Can anyone point to an implementation in JavaScript/TypeScript?

In general, it's been very difficult to implement App Attest on the server due to the lack of reference implementations provided by Apple.

Replies

I've done it in C++ using OpenSSL and/or Botan.

You need an ASN1 parser. I guess you already have one since you need it to decode the attestation. Have you been able to e.g. "pretty-print" the receipt? How far have you got?

  • I tried with asn1.js, but it seems that I need to specify the schema. I haven't been able to find any other library for JS/TS that does this. I'm really out of my depth here, and I can't believe Apple provides no resources that show how to do this on the server.

Add a Comment

I tried with asn1.js, but it seems that I need to specify the schema.

Maybe fromBER in parser.ts ?

I did consider trying to use Node.js, but decided that the required crypto primitives were not available in the version of Node.js that was available in AWS Lambda functions at the time. Maybe that has changed.

I'm really out of my depth here

Beware, this is quite difficult to get right even once you have implemented it correctly. There are lots of edge cases, including:

  • iOS apps running legitimately on Macs.
  • Users who have replaced their devices and restored from a backup, getting the key ID from the old device.
  • Users who have not used the app for a while and the App Store has discarded their risk information (you get 404 from the endpoint in this case).
  • More things that I haven't debugged yet.

Currently, I see far more failures than can be explained by people trying to crack the app, so the reported "risk metric" is of little use. You need to work out where the balance is between blocking access to a user with a cracked app versus blocking access to a genuine user who can write a bad review saying they can no longer access content that they have paid for. (And they will nearly always leave a bad app store review, rather than contacting you for support.)

If your balance is 10 bad reviews = 1 cracked app, maybe App Attest is useful; if it's 1 bad review = 10 cracked apps, it's not the right solution IMO.

You also need to decide what risk metric value is too risky, and Apple don't give us any advice on this. Currently my distribution is

Risk metric : % of users
1 : ~99%
2 : 1%
3 : 0.2%
4 : 0
5 : 0.15%
Device claims not to support App Attest : 0.25%

Should I be blocking the 0.15% of users with a risk metric of 5? Or does that distribution mean that all my users are OK, and only a risk metric of 100 is serious? We have no way of knowing.

Good luck, anyway.

Thanks for those insights. Maybe it's not worth using the fraud metric after all. Does the ability for a device to produce a valid attestation (apart from the fraud metric) give you any insight into whether the app is running on a jailbroken device or is otherwise compromised? I am at least able to validate attestations.

I did try using fromBER, but I wasn't sure what to do with the output:

Sequence {
  blockLength: 3774,
  error: '',
  warnings: [],
  valueBeforeDecodeView: Uint8Array(3774) [
    <values omitted>
  ],
  name: '',
  optional: false,
  idBlock: LocalIdentificationBlock {
    blockLength: 1,
    error: '',
    warnings: [],
    valueBeforeDecodeView: Uint8Array(0) [],
    isHexOnly: false,
    valueHexView: Uint8Array(0) [],
    tagClass: 1,
    tagNumber: 16,
    isConstructed: true
  },
  lenBlock: LocalLengthBlock {
    blockLength: 1,
    error: '',
    warnings: [],
    valueBeforeDecodeView: Uint8Array(0) [],
    isIndefiniteForm: true,
    longFormUsed: false,
    length: 0
  },
  valueBlock: LocalConstructedValueBlock {
    blockLength: 3772,
    error: '',
    warnings: [],
    valueBeforeDecodeView: Uint8Array(3772) [
      <values omitted>
    ],
    value: [ [ObjectIdentifier], [Constructed] ],
    isIndefiniteForm: true
  }
}

I did try using fromBER, but I wasn't sure what to do with the output:

That does have a lot of boilerplate nonsense....

I guess it hasn't decoded it recursively, so now you need to call it again on the valueBlock. Or something.

Does the ability for a device to produce a valid attestation (apart from the fraud metric) give you any insight into whether the app is running on a jailbroken device or is otherwise compromised?

As I understand it, it certainly gives you some level of trust. Yet Apple still chose to implement the risk metric. I think it is related to non-cracking abuse, e.g. single App Store accounts shared between very large numbers of devices. Or something.

After decoding the nested value like this:

    const result1 = fromBER(newReceiptBuffer);
    const result2 = fromBER(result1.result.valueBlock.valueBeforeDecodeView);
    console.log(result2.result.toJSON());

I get this result. Any idea how to proceed from here?

{
  blockName: 'OBJECT IDENTIFIER',
  blockLength: 11,
  error: '',
  warnings: [],
  valueBeforeDecode: <omitted hex value 32 characters in length>,
  idBlock: {
    blockName: 'identificationBlock',
    blockLength: 1,
    error: '',
    warnings: [],
    valueBeforeDecode: '',
    isHexOnly: false,
    valueHex: '',
    tagClass: 1,
    tagNumber: 6,
    isConstructed: false
  },
  lenBlock: {
    blockName: 'lengthBlock',
    blockLength: 1,
    error: '',
    warnings: [],
    valueBeforeDecode: '',
    isIndefiniteForm: false,
    longFormUsed: false,
    length: 9
  },
  valueBlock: {
    blockName: 'ObjectIdentifierValueBlock',
    blockLength: 9,
    error: '',
    warnings: [],
    valueBeforeDecode: '',
    value: '1.2.840.113549.1.7.2',
    sidArray: [ [Object], [Object], [Object], [Object], [Object], [Object] ]
  },
  name: '',
  optional: false,
  value: '1.2.840.113549.1.7.2'
}