App Store Notifications v2 - Verifying a signature

I have started implementing support for the new App Store Server notifications (version 2): https://developer.apple.com/documentation/appstoreservernotifications/receiving_app_store_server_notifications

I am not sure how to prevent a possible mad-in-the-middle attack when using those notifications. The decoded header that I get for notifications in the Sandbox environment is missing the "kid" field that is used to identify the key used to generate a signature.

Yes, I understand the the whole entire certificate chain is available in the "x5c" field and it could be verified by itself. However, this does not guarantee that a notification was signed by Apple.

This approach (with no specific key, with a certificate chain in x5c) works fine when verifying a receipt on device with StoreKit 2 but it does not work when getting a notification on a server.

Add a Comment

Replies

Any update on this?

Check this: https://gist.github.com/behe/25ddd9e873f36657776f69e6d4ea8ade

And this: https://stackoverflow.com/questions/69735525/how-to-verify-jws-transaction-of-app-store-server-api-in-go

Basically you can validate it with apples root certificate.

if you are using node there is this: https://github.com/agisboye/app-store-server-api

I'm using python in an environment where I can't install any new native libs.. but this seems to work (I just copied what the above project does, I have no idea if it's best practice etc)

import base64

from asn1crypto import x509, pem
from jose import jwt

_APPLE_ROOT_CA_G3_FINGERPRINT = "63 34 3A BF B8 9A 6A 03 EB B5 7E 9B 3F 5F A7 BE 7C 4F 5C 75 6F 30 17 B3 A8 C4 88 C3 65 3E 91 79"


def verifyAppleSignedPayload(jwsData):
    headers = jwt.get_unverified_headers(jwsData)
    certs = map(lambda x: x509.Certificate.load(base64.b64decode(x)), headers['x5c'])
    # root certificate is Apple ROOT CA G3
    root = certs[-1]
    if root.sha256_fingerprint != _APPLE_ROOT_CA_G3_FINGERPRINT:
        raise ValueError("Expected Apple Root CA G3, found {}".format(root.subject.human_friendly))
    # each cert in the list was issued by the next one
    for i, cert in enumerate(certs[:-1]):
        if cert.issuer.sha256 != certs[i+1].subject.sha256:
            raise ValueError("Cert chain not valid: {} not issued by cert {}".format(cert.subject.human_friendly, certs[i+1].subject.human_friendly))
    pk = pem.armor(u"PUBLIC KEY", certs[0].public_key.dump())
    return jwt.decode(jwsData, pk, 'ES256')
  • Just adding a comment to my above comment here since apple forums don't seem to allow editing or deleting posts (!) - anyway, do not do this ! it's not enough, it's not verifying the certificate chain. Any certs except the root one could be spoofed and this would work

  • `X509 parsing is a minefield, and the tooling built around it (much of it bindings to OpenSSL) has far too many knobs for the average developer to build safe applications. If you are expecting developers to implement certificate chain validation, providing explicit instructions is a minimum requirement, and providing tooling to do so is preferable. This is a rule of thumb that extends to any situation in which you are asking developers to perform some cryptographic step: we must ask "what knowledge is needed to do this correctly?", "what is the impact of misuse or improper implementation?", and "how can we reduce or eliminate the risk caused by lack of developer cryptographic knowledge?".

    Ideally, there should be no requirement that developers manually implement these steps: a service provider should maintain client libraries that implement all the necessary cryptographic checks without exposing intricacies to the developer when it's not necessary.`

    https://duo.com/labs/research/chain-of-fools

Add a Comment