如何使用PHP从Apple App Attestation对象中提取并验证证书?
解决App Attestation服务端验证的证书解析与后续验证问题
我之前在做App Attestation服务端验证的时候,也踩过和你一模一样的坑,给你一步步梳理解决思路和具体操作:
1. 先解决x5c证书的读取问题
你拿到的$norm['attStmt']['x5c'][0]是DER格式的二进制证书,而openssl_x509_read()默认只识别带-----BEGIN CERTIFICATE-----头和尾的PEM格式证书,直接读取自然会失败。你需要先把DER格式转成PEM格式:
// 处理叶证书(x5c数组第一个元素,即credcert) $derCert = $norm['attStmt']['x5c'][0]; // 把二进制DER证书转成Base64编码,再拼接PEM标准头尾 $pemCert = "-----BEGIN CERTIFICATE-----\n" . chunk_split(base64_encode($derCert), 64, "\n") . "-----END CERTIFICATE-----\n"; // 现在再读取证书就没问题了 $cert = openssl_x509_read($pemCert); if ($cert === false) { // 打印OpenSSL错误信息排查问题 echo openssl_error_string(); exit; } $parsedCert = openssl_x509_parse($cert, false);
用同样的方法可以把x5c里的中间证书也转成PEM格式,方便后续操作。
2. 验证证书链的有效性
接下来要验证整个证书链的合法性:叶证书 → 中间证书 → Apple App Attest根证书。
操作步骤:
- 提前获取
Apple App Attest Root CA - G1的PEM格式证书,存到服务端。 - 创建一个证书存储,把中间证书和根证书添加进去。
- 用这个存储验证叶证书,确保它是Apple合法签发的。
示例代码:
// 把x5c[1]的中间证书转成PEM格式 $intermediateDer = $norm['attStmt']['x5c'][1]; $intermediatePem = "-----BEGIN CERTIFICATE-----\n" . chunk_split(base64_encode($intermediateDer), 64, "\n") . "-----END CERTIFICATE-----\n"; // Apple App Attest根证书的PEM内容(需要提前获取完整内容) $rootPem = "-----BEGIN CERTIFICATE-----\nMIIB...(省略完整根证书内容)...-----END CERTIFICATE-----\n"; // 创建证书存储并添加证书 $certStore = openssl_x509_store_new(); openssl_x509_store_add_cert($certStore, openssl_x509_read($intermediatePem)); openssl_x509_store_add_cert($certStore, openssl_x509_read($rootPem)); // 验证叶证书 $verifyResult = openssl_x509_verify($cert, $certStore); if ($verifyResult !== 1) { echo "证书链验证失败:" . openssl_error_string(); exit; }
3. 验证证书的关键扩展字段
证书链验证通过后,还要检查叶证书的特定扩展字段,确保它是针对你的App签发的:
- App ID验证:查看证书的
1.2.840.113635.100.8.2扩展字段,这个值是你的App ID的SHA-256哈希,要和你本地计算的App ID哈希完全一致。 - Key ID验证:查看
1.2.840.113635.100.8.1扩展字段,这个值要和authData里解析出的key ID匹配。
示例代码:
// 获取App ID扩展字段(二进制格式) $appIdExtension = openssl_x509_parse($cert)['extensions']['1.2.840.113635.100.8.2']; // 计算你的App ID的SHA-256哈希(比如你的App ID是com.yourcompany.yourapp) $expectedAppIdHash = hash('sha256', 'com.yourcompany.yourapp', true); if ($appIdExtension !== $expectedAppIdHash) { echo "App ID不匹配"; exit; }
4. 解析并验证authData
authData是CTAP2格式的二进制数据,里面包含了设备和凭证的关键信息,需要解析:
- 前32字节:服务端域名(rpID)的SHA-256哈希,要和你的服务端域名哈希一致。
- 第33字节:flags字段,用于标识凭证状态。
- 第34-37字节:signCount,凭证的使用次数,用于防止重放攻击。
- 第38字节起:attestedCredentialData,包含Apple固定的aaguid、credentialID、公钥等信息。
你可以自己编写解析逻辑,或者用专门的CTAP2解析库来处理。
5. 验证Attestation签名
最后一步是验证attStmt里的sig签名:
- 构造待验证的原始数据:
authData+clientDataHash(客户端发送的挑战字符串的SHA-256哈希)。 - 用叶证书的公钥验证这个数据的签名(签名算法为ES256)。
示例代码:
// 假设你已经拿到客户端发送的挑战字符串clientData $clientDataHash = hash('sha256', $clientData, true); // 拼接待验证的原始数据 $verifyData = $norm['authData'] . $clientDataHash; // 获取叶证书的公钥 $publicKey = openssl_pkey_get_public($cert); // 验证签名(sig是二进制格式) $verifyResult = openssl_verify($verifyData, $norm['attStmt']['sig'], $publicKey, OPENSSL_ALGO_SHA256); if ($verifyResult !== 1) { echo "签名验证失败:" . openssl_error_string(); exit; }
调试小技巧
如果某一步失败,可以用OpenSSL命令行工具辅助排查:
- 把DER转成PEM后,执行
openssl x509 -in cert.pem -text -noout,查看证书内容是否正常解析。 - 用
openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem验证证书链是否有效。
内容的提问来源于stack exchange,提问作者Leo
相关产品推荐
相关产品推荐

