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

  • did you find a nice way to solve this?

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.

Add a Comment