php - 使用 PHP 验证来自 Android SafetyNet 的 JWS 响应
问题描述
概括
我能够从 Google 的服务器获取 JWS SafetyNet 证明并将其发送到我的服务器。服务器运行 PHP。
如何在我的服务器上使用 PHP“使用证书验证 JWS 消息的签名”?
我一直在做什么
我确实知道如何解码有效载荷并使用它,但我也想确保 JWS 没有被篡改。即https://developer.android.com/training/safetynet/attestation官方文档上的“验证 SafetyNet 证明响应”
我想使用一些已经制作的库/库来执行此操作,但我被卡住了。
起初我尝试使用https://github.com/firebase/php-jwt库和解码方法。问题是它需要一把钥匙,而我至今无法弄清楚它需要什么钥匙。我明白了PHP Warning: openssl_verify(): supplied key param cannot be coerced into a public key in ...
。所以,它需要一些公钥……某物……
官方文档有4点:
- 从 JWS 消息中提取 SSL 证书链。
- 验证 SSL 证书链并使用 SSL 主机名匹配来验证叶证书是否已颁发给主机名 attest.android.com。
- 使用证书来验证 JWS 消息的签名。
- 检查 JWS 消息的数据以确保它与您的原始请求中的数据相匹配。特别是,确保时间戳已经过验证,并且应用签名证书的随机数、包名称和哈希值与预期值匹配。
在互联网的帮助下,我可以做 1 和 2(至少部分):
list($header, $payload, $signature) = explode('.', $jwt);
$headerJson = json_decode(base64_decode($header), true);
$cert = openssl_x509_parse(convertCertToPem($headerJson['x5c'][0]));
...
function convertCertToPem(string $cert) : string
{
$output = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
$output .= chunk_split($cert, 64, PHP_EOL);
$output .= '-----END CERTIFICATE-----'.PHP_EOL;
return $output;
}
手动检查标题内容说它具有属性 alg 和 x5c。alg 可以用作解码调用的有效算法。x5c 有 2 个证书的列表,根据规范,第一个应该是第一个(https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-signature-36#section -4.1.5 )
我可以检查它匹配的证书的 CN 字段 $cert['subject']['CN'] === 'attest.android.com' 并且我还需要验证证书链(尚未处理还没有)。
但是如何使用证书来验证 jwt?
根据 如何使用公共 RSA 密钥验证 JSON Web 令牌? 该证书不是公开的,您可以:
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
$publicKey = $pkey_array ['key'];
但我使用我的 $cert 卡在第一行openssl_pkey_get_public(): key array must be of the form array(0 => key, 1 => phrase) in ...
笔记
我猜我至少需要来自 jws 数据之外的东西,比如公钥或其他东西......或者这是通过验证证书链到机器上的根证书来解决的?
我想让这项工作以生产方式进行,即在谷歌调用 api 来验证每个 jws 不是一个选项。
其他相关(?)我一直在阅读(在很多不相关的页面中):
- Android SafetyNet JWT 签名验证
- 使用客户端指纹编码 JWT 令牌?
- 如何解码 SafetyNet JWS 响应?
- 如何从 Android 应用程序中的标头数据验证 Safety Net JWS 签名 https://medium.com/@herrjemand/verifying-fido2-safetynet-attestation-bd261ce1978d
不再存在从某些来源链接的库:
解决方案
很晚了,但对于那些想知道的人来说
尝试使用解码签名base64Url_decode
下面的代码应该可以工作
$components = explode('.', $jwsString);
if (count($components) !== 3) {
throw new MalformedSignatureException('JWS string must contain 3 dot separated component.');
}
$header = base64_decode($components[0]);
$payload = base64_decode($components[1]);
$signature = self::base64Url_decode($components[2]);
$dataToSign = $components[0].".".$components[1];
$headerJson = json_decode($header,true);
$algorithm = $headerJson['alg'];
echo "<pre style='white-space: pre-wrap; word-break: keep-all;'>$algorithm</pre>";
$certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
$certificate .= chunk_split($headerJson['x5c'][0],64,PHP_EOL);
$certificate .= '-----END CERTIFICATE-----'.PHP_EOL;
$certparsed = openssl_x509_parse($certificate,false);
print_r($certparsed);
$cert_object = openssl_x509_read($certificate);
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
echo "<br></br>";
print_r($pkey_array);
$publicKey = $pkey_array ['key'];
echo "<pre style='white-space: pre-wrap; word-break: keep-all;'>$publicKey</pre>";
$result = openssl_verify($dataToSign,$signature,$publicKey,OPENSSL_ALGO_SHA256);
if ($result == 1) {
echo "good";
} elseif ($result == 0) {
echo "bad";
} else {
echo "ugly, error checking signature";
}
openssl_pkey_free($pkey_object);
private static function base64Url_decode($data)
{
return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}
推荐阅读
- python - 运行 Python 脚本时请求状态码 500
- ios - SwiftUI 列出多个自定义行编辑选项
- python - 如何在 Python 中运行下载的存储库的配置?
- flutter - 在下拉菜单中显示值时出错
- git - 特定分支的 Git 推送失败
- javascript - React 路由器没有像我需要的那样工作
- pandas - 如何对期间桶中的事件持续时间求和?
- excel - Excel VBA_ 对象工作表的方法范围失败(运行时错误 1004)
- python - 多处理与使用工人测功机
- android - 在片段中获取 null FragmentManager/Activity