使用 PHP 验证来自 Android SafetyNet 的 JWS 响应

Posted

技术标签:

【中文标题】使用 PHP 验证来自 Android SafetyNet 的 JWS 响应【英文标题】:Verify JWS response from Android SafetyNet using PHP 【发布时间】:2020-07-25 09:45:26 【问题描述】:

总结

我能够从 Google 的服务器获取 JWS SafetyNet 证明并将其发送到我的服务器。 服务器运行 php

如何在我的服务器上使用 PHP “使用证书验证 JWS 消息的签名”?

我一直在做什么

我确实知道如何解码有效载荷并使用它,但我还想确保 JWS 没有被篡改。 IE。 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?

根据 How do I verify a JSON Web Token using a Public RSA key? 该证书不是公开的,您可以:

$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 signature verification Use client fingerprint to encode JWT token? How to decode SafetyNet JWS response? How to validate Safety Net JWS signature from header data in Android apphttps://medium.com/@herrjemand/verifying-fido2-safetynet-attestation-bd261ce1978d

不再存在从某些来源链接的库:

https://github.com/cigital/safetynet-web-php

【问题讨论】:

您能否发布它为您提供的密钥以及您要验证的消息?那条消息的签名呢?我也许可以提供一些见解,而不必涉足智威汤逊水域。谢谢! 你有什么进展吗? 不,还没有@user3841429。我正在做其他事情。 Ping @user3841429 现在有一个接受的答案。 【参考方案1】:

很晚了,但对于想知道的人来说

尝试使用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));

【讨论】:

【参考方案2】:

我使用以下代码从 x509 证书获取公钥。但是签名验证总是失败。它是用于验证的正确公钥吗?无法发表评论,因此发布作为答案。

    $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 = base64_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);

【讨论】:

我当时尝试过,但从未成功过。谢谢,你在路上迈出了一步,但我正在向Poorya提供答案,因为它已经完成并且开箱即用。

以上是关于使用 PHP 验证来自 Android SafetyNet 的 JWS 响应的主要内容,如果未能解决你的问题,请参考以下文章

验证消息来自特定的应用程序/端点

AWS SES PHP SDK 请求需要来自经过验证的域?

如何使用 Java (Servlet) 验证来自 In-App Billing Android Market 的签名数据

来自 php 服务器的 Android 推送通知

在 PHP 的服务器端使用 facebook SDK 验证/验证 android 应用程序的用户访问权限?

在android中看不到来自php的JSON响应