I am able to get a JWS SafetyNet attestation from Google's server and send it to my server. The server runs PHP.
How do I "Use the certificate to verify the signature of the JWS message" using PHP on my server?
I do know how to just decode payload and use that, but I also want to make sure the JWS has not been tampered with. I.e. "Verify the SafetyNet attestation response" on the official documentations at https://developer.android.com/training/safetynet/attestation
I want to use some already made library/libraries for doing this but I get stuck.
At first I tried using the https://github.com/firebase/php-jwt library and the decode-method. The problem is that it wants a key, and I have so far been unable to figure out what key it needs. I get PHP Warning: openssl_verify(): supplied key param cannot be coerced into a public key in ...
. So, it wants some public key... of something...
The offical doc has 4 points:
I can do 1 and 2 (partially at least), with the help of internet:
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;
}
Manually checking header content says it has attributes alg and x5c. alg can be used as valid algorithm to the decode-call. x5c has a list of 2 certs, and according to the spec the first one should be the one (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-signature-36#section-4.1.5)
I can check the CN field of the certificate that it matches, $cert['subject']['CN'] === 'attest.android.com' and I also need to validate the cert chain (have not been working on that yet).
But how do I use the certificate to validate the jwt?
According to How do I verify a JSON Web Token using a Public RSA key? the certificate is not the public one and that you could:
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
$publicKey = $pkey_array ['key'];
but I get stuck on the first line using my $cert openssl_pkey_get_public(): key array must be of the form array(0 => key, 1 => phrase) in ...
I guessed I needed at least something from outside the jws data, like a public key or something... or is this solved by the validation of the cert chain to a root cert on the machine?
I want to make this work production-wise, i.e. calling the api at google to verify every jws is not an option.
Other related(?) I have been reading (among a lot of unrelated pages too):
No longer existing lib that is linked from some sources:
quite late but for people who wonder
try decoding signature using base64Url_decode
below code should work
$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));
}