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

Verify JWS response from Android SafetyNet using PHP

总结

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

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

我一直在做什么

我确实知道如何解码有效负载并使用它,但我也想确保 JWS 未被篡改。 IE。 https://developer.android.com/training/safetynet/attestation

官方文档中的“验证 SafetyNet 认证响应”

我想使用一些已经制作好的 library/libraries 来做这个,但我卡住了。

起初我尝试使用 https://github.com/firebase/php-jwt 库和 decode-method。问题是它需要一把钥匙,而我到目前为止还无法弄清楚它需要什么钥匙。我得到 PHP Warning: openssl_verify(): supplied key param cannot be coerced into a public key in ...。所以,它需要一些 public 键...一些...

官方文档有4分:

  1. 从 JWS 消息中提取 SSL 证书链。
  2. 验证 SSL 证书链并使用 SSL 主机名匹配来验证叶证书是否已颁发给主机名 attest.android.com.
  3. 使用证书验证 JWS 消息的签名。
  4. 检查 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;
    }  

手动检查 header 内容表明它具有属性 alg 和 x5c。 alg 可以用作 decode-call 的有效算法。 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?

根据 该证书不是 public 证书,您可以:

$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 数据之外的东西,比如 public 密钥之类的……或者这是否可以通过验证机器上的根证书的证书链来解决?

我想让这个工作 production-wise,即在 google 调用 api 来验证每个 jws 不是一个选项。

其他相关(?)我一直在阅读(也在很多不相关的页面中):

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

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

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

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

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