How to verify x5c JWT signatures with Firebase's php-jwt, how to decode?

I'm using the App Store Server API to fetch information about in-app purchases/subscriptions. I'm able to fetch data successfully, but I can't seem to verify/decode the signed transactions that come back. (I know that I can split and base64 decode to get the payload, but I need to verify the the JWT to ensure the security.)

$signedTransactionJWT = $response['signedTransactions'][0];
$privateKeyText = file_get_contents('/private/key/from/appstoreconnect.p8');
$decodedTransactionPayload = JWT::decode($signedTransactionJWT, new Key($privateKeyText, 'ES256'));

but that gave me:

openssl_verify(): supplied key param cannot be coerced into a public key

I'm trying to figure out how to do this verification. The WWDC videos about it say that since Apple is using an x5c JWT header, that the token should be self-contained and verifiable without need for external resources. I've seen some other pages say that I need to fetch certificates or JWKS from Apple and use those, but a) that didn't work for me either, and b) again, I thought the x5c was supposed to make it internally verifiable.

Any help?

Answered by kennywyland in 732210022

Figured it out with some help on Stack Overflow. The JWT returned by Apple isn't signed with my private key, it is signed with Apple's public key. That public key is embedded in the certificate in the header['x5c'] array. I had to convert that base64-encoded-DER certificate data into PEM format (which just means wrapping it in some text). Then I could extract the public key from that PEM certificate. THEN I could use that to do the verify/decode.

list($headerb64, $bodyb64, $cryptob64) = explode('.', $jwt);
$headertext = JWT::urlsafeB64Decode($headerb64);
$header = JWT::jsonDecode($headertext);
$dercertificateb64 = $header->x5c[0];
$wrappedcertificatetext = trim(chunk_split($dercertificateb64, 64));
$certificate = <<<EOD
-----BEGIN CERTIFICATE-----
$wrappedcertificatetext
-----END CERTIFICATE-----
EOD;

$publickey = openssl_pkey_get_public($certificate);
$decoded = JWT::decode($jwt, new Key($publickey, $header->alg));

This isn't the final step though, this just verifies the internal consistency of the first element in the JWT, but doesn't test whether it really comes from Apple. The other elements in the header['x5c'] array are the certificate chain needed to complete the verification of the chain. I haven't figured out how to do that part yet.

Accepted Answer

Figured it out with some help on Stack Overflow. The JWT returned by Apple isn't signed with my private key, it is signed with Apple's public key. That public key is embedded in the certificate in the header['x5c'] array. I had to convert that base64-encoded-DER certificate data into PEM format (which just means wrapping it in some text). Then I could extract the public key from that PEM certificate. THEN I could use that to do the verify/decode.

list($headerb64, $bodyb64, $cryptob64) = explode('.', $jwt);
$headertext = JWT::urlsafeB64Decode($headerb64);
$header = JWT::jsonDecode($headertext);
$dercertificateb64 = $header->x5c[0];
$wrappedcertificatetext = trim(chunk_split($dercertificateb64, 64));
$certificate = <<<EOD
-----BEGIN CERTIFICATE-----
$wrappedcertificatetext
-----END CERTIFICATE-----
EOD;

$publickey = openssl_pkey_get_public($certificate);
$decoded = JWT::decode($jwt, new Key($publickey, $header->alg));

This isn't the final step though, this just verifies the internal consistency of the first element in the JWT, but doesn't test whether it really comes from Apple. The other elements in the header['x5c'] array are the certificate chain needed to complete the verification of the chain. I haven't figured out how to do that part yet.

How to verify x5c JWT signatures with Firebase's php-jwt, how to decode?
 
 
Q