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.

I also have the same doubts

Did you solve it?

Any update on this?

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')

I have exactly the same question. I took half a day to figure out how to verify the cert chain in x5c, couldn't make it until I found this question.

It's really bad work as there's nothing described in the official guide https://developer.apple.com/documentation/appstoreservernotifications Not mentioned in WWDC21 session as well https://developer.apple.com/videos/play/wwdc2021/10174/

full solution with composer dependency [https://github.com/gobran1/AppleVerifyNotificationSignature/tree/main] here is my code in php and is working correctly public function verifySignature($jws) { $data = explode(".", $jws);

    //getting header from cert
    $certs = json_decode(base64_decode($data[0]))->x5c;

    //validate that each certificate is issued and by next one in chain
    $chain_validation = true;
    for ($i = 0; $i < count($certs) - 1; $i++) {
        $x509 = new X509();
        $x509->loadX509($certs[$i]);
        $x509->loadCA($certs[$i + 1]);
        $chain_validation &= $x509->validateSignature() & $x509->validateDate();
    }


    //verify fingerprint that equal to G3 Apple Certificate fingerprint that we can get from apple PKI certificates
    $cert_resource = openssl_x509_read("-----BEGIN CERTIFICATE-----\n" . $certs[count($certs) - 1] . "\n-----END CERTIFICATE-----");
    $certificateFingerprint = openssl_x509_fingerprint($cert_resource, 'sha256', true);
    $fingerprintString = strtoupper(implode(':', str_split(bin2hex($certificateFingerprint), 2)));
    $chain_validation &= ($fingerprintString === self::APPLE_ROOT_CA_G3_FINGERPRINT);


    //validate jws signature base on public key that we fetched from first certificate

    //read public key
    $jwk = JWKFactory::createFromCertificate(
        "-----BEGIN CERTIFICATE-----\n" . $certs[0] . "\n-----END CERTIFICATE-----",
        ['use' => 'sig']
    );

    //deserialize jws certificate
    $jws = (new CompactSerializer())->unserialize($jws);

    $algorithmManager = new AlgorithmManager([new ES256()]);
    $chain_validation &= (new JWSVerifier($algorithmManager))->verifyWithKey($jws, $jwk, 0);

    if (!$chain_validation)
        throw new CustomException("certificate validation failed", 400);
}

As Apple now offers an official solution to this, I thought it's best to bring this up again and give a more detailed explanation on how to solve the problem, in case someone is still searching for it.

Basically, the x5c header always contains 3 certificates but you are only really interested in the first two. The first certificate is the "leaf" certificate and the second one is the "intermediate" certificate. The chain is completed with the root certificate, which you need to download from the Apple PKI (use the Apple Root CA certificates). To verify the chain, you have to verify the leaf certificate with the intermediate certificate and the intermediate certificate with the root certificate from Apple.

Now let me introduce another attack vector: Private signing keys are highly valuable and the more often a key is used, the more likely it is that it might be compromised at some point. Imagine an attacker is able to obtain a private key belonging to a leaf certificate from Apple. They would now be able to sign their own server notifications and not only the signature, but also the certificate chain would be valid, because after all, a valid chain cannot be cryptographically invalidated. The certificate will expire at some point but until then, it would be viewed as valid. That's where the Online Certificate Status Protocol (OCSP) comes in. The OCSP allows you to check online, whether a certificate was revoked. To do this, the certificates contain an OCSP URI with which you can request the status of the certificate. The response itself is signed again with the issuer certificate. In case of the leaf certificate, the intermediate certificate is the issuer, while in case of the intermediate certificate, the root certificate is.

So the full verification process would be as follows:

  • Download the Apple Root CA certificate and load it into your system as the trusted entity.
  • Get the first two certificates from the x5c header. The first certificate is the "leaf" and the second is the "intermediate".
  • Verify the leaf certificate with the intermediate certificate.
  • Verify the intermediate certificate with the root certificate.
  • Get the status of the leaf certificate using the OCSP, check that the certificate is explicitly not revoked, and verify that the response is signed with the intermediate certificate.
  • Get the status of the intermediate certificate using the OCSP, check that the certificate is explicitly not revoked, and verify that the response is signed with the root certificate.
  • Get the public key from the leaf certificate and verify the payloads signature with it.

Sound complicated? Well, it is. So it's best if you don't implement it yourself but use a secure library instead. Since the WWDC 2023, Apple offers an official library in Swift, Java, Python, and Node.js, and I implemented it in PHP.

Can someone please expand on what it means when the method to load root certificates says 'implementation may vary'? I am absolutely clueless beyond just doing a readFile on the certificate that I downloaded and creating a buffer out of it. What else would that method entail? What should I read up on to figure this out? It also reads loadRootCAs in the libraries suggested in a comment, where the 's' in the end makes me think I'm supposed to read multiple certificate files and add their buffer values in a array to be passed with the decoder method?

App Store Notifications v2 - Verifying a signature
 
 
Q