如何使用PHP解密Apple Server-To-Server通知?
App Store Server Notifications v2 后续操作指引
我折腾了好几天,不确定是Apple的官方文档不完善还是缺少代码示例导致难度陡增,而且网上几乎找不到优质示例。目前已经完成以下步骤:
- 在App信息中添加沙箱Webhook,通过ngrok隧道运行以接收信息;
- 摸索后确定需要对响应内容进行Base64 URL解码,得到如下结构的对象:
Array ( [headers] => stdClass Object ( [alg] => ES256 [x5c] => Array ( [0] => ..., [1] => ..., [2] => ..., ) ) [payload] => stdClass Object ( [notificationType] => DID_RENEW [notificationUUID] => ...uuid... [data] => stdClass Object ( [bundleId] => ...app.id... [bundleVersion] => 1.5 [environment] => Sandbox [signedTransactionInfo] => ... //long string of encrypted info ) [version] => 2.0 [signedDate] => 1670890743027 ) [signature] => ... // signature string )
各关键字段说明:
headers->alg:使用的算法headers->x5c:包含3个元素的数组,对应Apple的证书链(按顺序):- 包含App Store签名JWS对应公钥的证书;
- Apple PKI站点的Worldwide Developer Relations中级证书;
- Apple根证书
payload:包含相关信息,其中payload->data->signedTransactionInfo为加密的交易信息;signature:签名字符串,用于验证JWS的合法性
我理解需要使用响应中的密钥解密JWT,尝试了web-token PHP库,但不知如何继续,仅知晓使用ES256算法,以下是后续操作指引:
核心操作流程
1. 验证Webhook通知的合法性
Apple的Webhook通知是JWS格式,必须验证签名确保来源可信:
- 从
headers->x5c数组中取出第一个证书,将其转换为标准PEM格式:在证书内容前后添加-----BEGIN CERTIFICATE-----和-----END CERTIFICATE-----,并按每行64字符分割内容 - 使用ES256算法,结合转换后的公钥证书,对原始Webhook响应的JWS字符串(不是你解码后的数组)进行签名验证
2. 解析加密的交易信息
signedTransactionInfo本身也是一个JWT,同样需要验证并解析:
- 用ES256算法,结合Apple根证书(可从
x5c数组的第三个元素转换为PEM格式)验证该JWT的签名 - 验证通过后解码JWT,即可获取完整的交易详情(如交易ID、订阅到期时间、续期状态等)
使用web-token库的代码示例
use Jose\Component\Core\AlgorithmManager; use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\JWSVerifier; use Jose\Component\Signature\Serializer\CompactSerializer; use Jose\Component\KeyManagement\JWKFactory; // 初始化算法与验证器 $algorithmManager = AlgorithmManager::create([new ES256()]); $jwsVerifier = new JWSVerifier($algorithmManager); $serializer = new CompactSerializer(); // 获取原始Webhook的JWS字符串(假设请求体中字段为signedPayload) $rawJws = $_POST['signedPayload']; // 解析JWS并提取证书链 $jws = $serializer->unserialize($rawJws); $protectedHeader = $jws->getSignature(0)->getProtectedHeader(); $x5cChain = $protectedHeader['x5c']; // 将第一个证书转换为JWK格式 $publicCert = "-----BEGIN CERTIFICATE-----\n" . chunk_split($x5cChain[0], 64) . "-----END CERTIFICATE-----"; $publicKey = JWKFactory::createFromCertificate($publicCert); // 验证通知签名 $isNotificationValid = $jwsVerifier->verifyWithKey($jws, $publicKey, 0); if (!$isNotificationValid) { die("Invalid Apple notification signature"); } // 解析通知Payload $notificationPayload = json_decode($jws->getPayload(), true); // 解析加密的交易信息 $signedTransactionJws = $notificationPayload['data']['signedTransactionInfo']; $transactionJws = $serializer->unserialize($signedTransactionJws); // 转换根证书为JWK $rootCert = "-----BEGIN CERTIFICATE-----\n" . chunk_split($x5cChain[2], 64) . "-----END CERTIFICATE-----"; $rootPublicKey = JWKFactory::createFromCertificate($rootCert); // 验证交易信息签名 $isTransactionValid = $jwsVerifier->verifyWithKey($transactionJws, $rootPublicKey, 0); if ($isTransactionValid) { $transactionData = json_decode($transactionJws->getPayload(), true); print_r($transactionData); // 输出完整交易详情 }
关键注意事项
- 必须使用原始JWS字符串进行签名验证,不能用解码后的数组,因为签名是针对整个JWS生成的
- 证书必须转换为标准PEM格式,否则无法被库正常识别
- 沙箱与生产环境的证书链不同,需注意区分环境
- 签名验证是强制安全步骤,绝对不能跳过
内容的提问来源于stack exchange,提问作者Mihail Minkov
相关产品推荐
相关产品推荐

