Verify JWS response from Android SafetyNet using PHP - php

Summary
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?
What I have been doing
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:
Extract the SSL certificate chain from the JWS message.
Validate the SSL certificate chain and use SSL hostname matching to verify that the leaf certificate was issued to the hostname
attest.android.com.
Use the certificate to verify the signature of the JWS message.
Check the data of the JWS message to make sure it matches the data within your original request. In particular, make sure that the
timestamp has been validated and that the nonce, package name, and
hashes of the app's signing certificate(s) match the expected
values.
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 ...
Notes
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):
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 app https://medium.com/#herrjemand/verifying-fido2-safetynet-attestation-bd261ce1978d
No longer existing lib that is linked from some sources:
https://github.com/cigital/safetynet-web-php

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

I got public key from x509 certificate using below code. But signature validation always fail. Is it the correct public key for verification? Can't post comment so posting as an answer.
$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);

Related

Need help understanding php signature verification

I am trying to verify a signature in php, and have exhausted myself trying every example I have found on the net. I've gone round in circles so much I have probably missed the solution now.
So I have test data which is
$msg = "test data";
which produced this signature using the private key from my key pair
$signature = "avALtk00btVyV74e5UdXJ/VClVV/fsuoLZpXQjiCrkVijsmMZsYWZujN56+Aa2CEQYkomDsm9CJ/Tue7lNP0tYVZz9Y0RngpcV9VT9V3i+3rbvbBEnuJuS/5e+PR7kQGMh8rVuCtHpAJhSePMyipC3kM90EQJ0jyY3rFaHDNpSzVBpOnRYLzqbsdy45v0bN78A2J/HaIhJy87Sh4X1a+WMg9PLkqSSYZnRYOB8XVDCYfyeeekcvI4rvP51wBQcaLwu7S0xPQA8yHfJqMXCqdmBVUQZrk/X+CujdXUyJItDWA8j2N8AHmcAD5oRaJ6bX3zCQFM1QnKMi1ETLudzIqfA==";
and this is the public key from the signing key pair
$key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxv4nCiH4vXvSLsvlceCOk3yfH1EQgNqNaVGdnFxdw9IIjSVZvTVH45NCodCJ0GlHoDwQM7DMV1+QrtF91cn44xg4Ys9zr1xkaT4jWBTe3YKoTqJoLHR4UU03F6Y1jTELhjY2a2Kt0ijyvAOKM4bm3gCItfMx59ETGInz7Oubb1T4IJ8TuWmZsh+X57c6fgv0B2+eTr/5FMK2VxXV5tHkB9UNLBgnbw0IZuC6izF4OFk9hxgh96i5wCf2HhHaNoEryx7ZV2ZG9a0OQnYZ+x1zaOIw6dJkV7rip3H57ksQfoQWM0GKMBB7cWIgWsf/GlbYTVgw26MvzEzGPb9uCfx8rwIDAQAB";
I have tried wrapping the key with this
$pubkey = "-----BEGIN RSA PUBLIC KEY-----" . $key . "-----END RSA PUBLIC KEY-----";
and with this
$pubkey = "-----BEGIN PUBLIC KEY-----" . $key . "-----END PUBLIC KEY-----";
I have tried creating a public key id with both wrappings and without any wrapping, like this
$pubkeyid = openssl_pkey_get_public($pubkey);
$pubkeyid = openssl_pkey_get_public($key);
and I have tried verifying the signature with $key and $pubkeyid, with various algorithms and with none, like this
openssl_verify($msg, base64_decode($signature), $pubkeyid);
openssl_verify($msg, base64_decode($signature), $key);
openssl_verify($msg, base64_decode($signature), $pubkeyid, "sha256withRSAEncryption");
openssl_verify($msg, base64_decode($signature), $key, "sha256withRSAEncryption");
openssl_verify($msg, base64_decode($signature), $pubkeyid, OPENSSL_ALGO_SHA256);
openssl_verify($msg, base64_decode($signature), $key, OPENSSL_ALGO_SHA256);
I probably tried some other permutations, but can't remember now. My head hurts.
No matter what I tried, I have not managed to verify the signature. I can verify the signature using the public key in java easily.
I loathe asking for a working php example because I have tried so many I have already found on the net and just can't get them to work. Unfortunately phpseclib is not an option for me, so I have to use openssl.
Where am I going wrong?
Thanks to #miken32 I have now finally fixed the code. Turns out all I was missing was a couple of line breaks when formatting the PEM key. So the final, and very simple code is:
// Get base64 encoded public key.
// NOTE: this is just for testing the code, final production code stores the public key in a db.
$pubkey = $_POST['pubkey'];
// Convert pubkey in to PEM format (don't forget the line breaks).
$pubkey_pem = "-----BEGIN PUBLIC KEY-----\n$pubkey\n-----END PUBLIC KEY-----";
// Get public key.
$key = openssl_pkey_get_public($pubkey_pem);
if ($key == 0)
{
$result = "Bad key zero.";
}
elseif ($key == false)
{
$result = "Bad key false.";
}
else
{
// Verify signature (use the same algorithm used to sign the msg).
$result = openssl_verify($msg, base64_decode($signature), $key, OPENSSL_ALGO_SHA256);
if ($result == 1)
{
$result = "Verified";
}
elseif ($result == 0)
{
$result = "Unverified";
}
else
{
$result = "Unknown verification response";
}
}
// do something with the result.

How do I verify a Google idToken in PHP?

I have been working on this for far too long. I am looking for a working example as of September 2016 for verifying a Google idToken such as
eyJhbGciOiJSUzI1NiIsImtpZCI6IjZjNzgxOTQyZDg0OWJhMmVjZGE4Y2VkYjcyZDM0MzU3ZmM5NWIzMjcifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiIxMDQ5MTQ4MTU2NTQ2LTk2YjFxcTJsNTJtODVtODB0ZHVoZHVma2RwODRtN2tuLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyNTk4NDgzNjQ2MjY1OTYxNTQwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF6cCI6IjEwNDkxNDgxNTY1NDYtdjJwZjRlbGhzOGNwcXBlcWZkMzU5am5nOWs5aW5kcTQuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJlbWFpbCI6InRlc3R1c2VydGh4QGdtYWlsLmNvbSIsImlhdCI6MTQ3NDc1NDMzMiwiZXhwIjoxNDc0NzU3OTMyLCJuYW1lIjoiVGVzdCBVc2VyIiwicGljdHVyZSI6Imh0dHBzOi8vbGg0Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tU0dldkZZRDlaWFEvQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQVBhWEhoUmtuX1hEaEhNLTEzeVMwTUtBcFNrZG1zVEdYdy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImxvY2FsZSI6ImVuIn0.btukbBvhek6w14CrBVTGs8X9_IXIHZKpV1NzJ3OgbGUfmoRMirNGzZiFAgrR7COTeDJTamxRzojxxmXx6EEkQqNQcbyN8dO0PTuNt9pujQjLbFw_HBhIFJQaJSR3-tYPN-UtHGQ5JAAySsvCPapXbxyiKzTyvGYRSU65LmyNuiGxe6RQe1zHjq2ABJ4IPRqKPuFupnGRPWYyBSTPU7XQvtfhgyqA0BWZUfmCIFyDxQhvMaXNLTs01gnGVhcUDWZLi9vuUiKUlz3-aSSbwdfCMAljhBHnjpYO6341k5-qmgKkWawv8DX_nMEzntsCMCr664rP4wFEbsRB5ledM9Pc9Q
Using Google's recommended way and pulling "accounts.google.com/.well-known/openid-configuration" for the jwks_uri and pulling that "www.googleapis.com/oauth2/v3/certs", yielding a relevant entry for
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "6c781942d849ba2ecda8cedb72d34357fc95b327",
"n": "s1dt5wFFaYl-Bt7Yb7QgWEatLJfxwWDhbd5yvm2Z4d1PRgNVQa9kwOArQNoOJ-b-oZnXLVFsVASUXEAumGf1ip5TVCQmMBKqlchSDNuoZfoWdpCCX7jx4gNuS43pS6VqV3QDjWnoXRTHaUi5pZEbpAmWpOeG_CfmewNVwBXPFx8-mtvEdtxIrspX4ayXTViR4vHc7MhQhUxllFbocxMjJysDQuZV9wN3MI0lVtQdf52SKJwF3lhvWA9-WAEZ1q8wq-I93Sfte95RaFjDqCH--Sh-8DjhK4OvgItcEGd5QRHjdLvrayPwaDQbpMRN2n3BkVWIxKJubtRiSeWbawCklQ",
"e": "AQAB"
}
Verification happens if I pass the token to https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=TOKEN, but this is not a real answer, since they don't change that often but doing an extra web call every time is just asking for trouble.
So can someone point me to a working example? I've tried phpseclib, but it never verifies. I've probably looked for about 40 hours at this point, and I'm at my wits' end.
Any help is appreciated.
My relevant code:
$modulus = "";
$exponent = "";
$token = $_POST['token'];
$pieces = explode(".", $token);
$header = json_decode(base64_decode(str_replace(['-','_'], ['+','/'], $pieces[0])), true);
$alg = $header['alg'];
$kid = $header['kid'];
$payload = base64_decode(str_replace(['-','_'], ['+','/'], $pieces[1]));
$signature = str_replace(['-','_'], ['+','/'], $pieces[2]);
//$signature = base64_decode(str_replace(['-','_'], ['+','/'], $pieces[2]));
if (testGoogleList($alg, $kid, $modulus, $exponent))
{
echo "Found in list: kid=".$kid."\n";
echo "n: (base64URL)".$modulus."\n";
echo "e: (base64URL)".$exponent."\n";
$modulus = str_replace(['-','_'], ['+','/'], $modulus);
$exponent = str_replace(['-','_'], ['+','/'], $exponent);
echo "n: (base64)".$modulus."\n";
echo "e: (base64)".$exponent."\n";
$rsa = new Crypt_RSA();
$rsa->setHash("sha256");
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
$modulus = new Math_BigInteger($modulus, 256);
$exponent = new Math_BigInteger($exponent, 256);
echo "n: (BigInteger)".$modulus."\n";
echo "e: (BigInteger)".$exponent."\n";
$rsa->loadKey(array('n' => $modulus, 'e' => $exponent));
$rsa->setPublicKey();
$pubKey = $rsa->getPublicKey();
echo "Public Key from phpseclib\n".$pubKey."\n";
echo "--First openSSL error check--\n";
while ($msg = openssl_error_string())
echo $msg . "<br />\n";
echo "--After First Error Check, before Verify--\n";
$res = $rsa->verify($pieces[0].".".$pieces[1], $signature);
while ($msg = openssl_error_string())
echo $msg . "<br />\n";
echo "--Verify result: ".var_export($res, true)."--\n";
}
Output
Found in list: kid=6c781942d849ba2ecda8cedb72d34357fc95b327
n: (base64URL)s1dt5wFFaYl-Bt7Yb7QgWEatLJfxwWDhbd5yvm2Z4d1PRgNVQa9kwOArQNoOJ-b-oZnXLVFsVASUXEAumGf1ip5TVCQmMBKqlchSDNuoZfoWdpCCX7jx4gNuS43pS6VqV3QDjWnoXRTHaUi5pZEbpAmWpOeG_CfmewNVwBXPFx8-mtvEdtxIrspX4ayXTViR4vHc7MhQhUxllFbocxMjJysDQuZV9wN3MI0lVtQdf52SKJwF3lhvWA9-WAEZ1q8wq-I93Sfte95RaFjDqCH--Sh-8DjhK4OvgItcEGd5QRHjdLvrayPwaDQbpMRN2n3BkVWIxKJubtRiSeWbawCklQ
e: (base64URL)AQAB
n: (base64)s1dt5wFFaYl+Bt7Yb7QgWEatLJfxwWDhbd5yvm2Z4d1PRgNVQa9kwOArQNoOJ+b+oZnXLVFsVASUXEAumGf1ip5TVCQmMBKqlchSDNuoZfoWdpCCX7jx4gNuS43pS6VqV3QDjWnoXRTHaUi5pZEbpAmWpOeG/CfmewNVwBXPFx8+mtvEdtxIrspX4ayXTViR4vHc7MhQhUxllFbocxMjJysDQuZV9wN3MI0lVtQdf52SKJwF3lhvWA9+WAEZ1q8wq+I93Sfte95RaFjDqCH++Sh+8DjhK4OvgItcEGd5QRHjdLvrayPwaDQbpMRN2n3BkVWIxKJubtRiSeWbawCklQ
e: (base64)AQAB
n: (BigInteger)18674717054764783973087488855176842456138281065703345249166514684640666364313492818979675328236363014396820758462507776710767978395332237045824933690552916871072924852353561300648679961653291310130667565640227949181785672954620248276915721938277908962537175894062430220752771265500386404609948390377043762106166027544443459977210114747088393335234720657330424186435226141073425445733987857419933850994487913462193466159335385639996611717486282518255208499657362420183528330692236194252505592468150318350852955051377118157817611947817677975817359347998935961426571802421142861030565807099600656362069178972477827638867161671399657071319083914500667014214521757304661303525496653078786180348831678824969667950119891369610525474165187687495455755684504105433077872587114630537058768184460798470456362909589578101896361255070801
e: (BigInteger)1095844162
Public Key from phpseclib
-----BEGIN PUBLIC KEY-----
MIIBeDANBgkqhkiG9w0BAQEFAAOCAWUAMIIBYAKCAVZzMWR0NXdGRmFZbCtCdDdZ
YjdRZ1dFYXRMSmZ4d1dEaGJkNXl2bTJaNGQxUFJnTlZRYTlrd09BclFOb09KK2Ir
b1puWExWRnNWQVNVWEVBdW1HZjFpcDVUVkNRbU1CS3FsY2hTRE51b1pmb1dkcEND
WDdqeDRnTnVTNDNwUzZWcVYzUURqV25vWFJUSGFVaTVwWkVicEFtV3BPZUcvQ2Zt
ZXdOVndCWFBGeDgrbXR2RWR0eElyc3BYNGF5WFRWaVI0dkhjN01oUWhVeGxsRmJv
Y3hNakp5c0RRdVpWOXdOM01JMGxWdFFkZjUyU0tKd0YzbGh2V0E5K1dBRVoxcTh3
cStJOTNTZnRlOTVSYUZqRHFDSCsrU2grOERqaEs0T3ZnSXRjRUdkNVFSSGpkTHZy
YXlQd2FEUWJwTVJOMm4zQmtWV0l4S0p1YnRSaVNlV2Jhd0NrbFECBEFRQUI=
-----END PUBLIC KEY-----
--First openSSL error check--
--After First Error Check, before Verify--
error:0906D06C:PEM routines:PEM_read_bio:no start line
--Verify result: false--
So for anyone coming here from search engines:
I was trying to use a Google ID Token to verify that my login credentials were:
Accurate
Not spoofed
Able to be checked by a back-end server
Computed using math so I don't have to query Google each time (adding latency and the if-anything-can-go-wrong-it-will effect)
I realize that most will certainly be able to read this code, but I wanted to type it up to explain what's happening for the next exasperated soul.
Your first part may vary, since I was coming from Android and it's fairly straight-forward from there.
My process was to ask for the token in Android.
(Only differences from examples and relevant pieces shown)
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.client_id))
.requestEmail()
.build();
Getting the token from the Activity Result (onActivityResult)
GoogleSignInAccount acct = result.getSignInAccount();
String idToken = acct.getIdToken();
The token is composed of 3 pieces, separated by periods, in the form "$header.$info.$signature". We will verify "$header.$info" using "$signature" to do so.
The $header contains information about the encryption, for example (after decoding):
{"alg":"RS256","kid":"6c781942d849ba2ecda8cedb72d34357fc95b327"}
So the algorithm used is "SHA-256, with RSA Encryption", and the Key ID in the keystore is 6c781942d849ba2ecda8cedb72d34357fc95b327. We'll use this later.
Pass the whole token to my back-end server via HTTP
Then decode the token using the following code, taken straight from the accepted answer
include('Crypt/RSA.php'); //path to phpseclib
$modulus = "";
$exponent = "";
$token = $_POST['token'];
$pieces = explode(".", $token);
$data = $pieces[0].".".$pieces[1];
$signature = base64_decode(str_replace(['-','_'], ['+','/'], $pieces[2]));
$header = json_decode(base64_decode(str_replace(['-','_'], ['+','/'], $pieces[0])), true);
$alg = $header['alg'];
$kid = $header['kid'];
if (testGoogleList($alg, $kid, $modulus, $exponent))
{
$modulus = base64_decode(str_replace(['-','_'], ['+','/'], $modulus));
$exponent = base64_decode(str_replace(['-','_'], ['+','/'], $exponent));
$rsa = new Crypt_RSA();
$rsa->loadKey([
'n' => new Math_BigInteger($modulus, 256),
'e' => new Math_BigInteger($exponent, 256)
]);
$rsa->setHash('sha256');
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
if ($rsa->verify($data, $signature))
{
echo "VALID!!!!";
} else {
echo "NOT VALID :'(";
}
}
The reason we do base64_decode(str_replace(['-','_'], ['+','/'], $VARIABLE)) is because these are presented in base64URL form, where the '+' is changed to a '-' and the '/' is changed to a '_'. So we change it from base64URL > base64 > unencoded (plain) text.
What does this do?
We take the token from $_POST (I called it $token).
Then we split it into its parts.
Remember we need to use the third part to decode the pair of the first two, separated by a period ("."). ("$signature" is the cryptographic signature for "$header.$info")
Fully decode the signature, from base64URL to unencoded (plain) text.
Since Google uses JSON to store the key information, json_decode the header and get the encryption type and key id.
I wrapped it in a function, but my function testGoogleList basically works like this:
So we pass in the algorithm and the key id.
I test my local cache of keys to see if the key we need is already cached.
If not, we continue here, otherwise skip ahead to step 4.
Then we hit the web and grab Google's open-id configuration page at (https://accounts.google.com/.well-known/openid-configuration) using get_file_contents() or a CURL method if you can't. I had to use CURL with options "curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANYSAFE);" in my CURL method, since it wasn't trying HTTPS correctly.
That page is a JSON encoded text file, so json_decode it.
We them grab the "jwks_uri" key and grab that page like we did above.
This contains a set of keys Google is currently using for public key verification. I json_decode and temporarily store these to an array.
Truncate your old cache and rewrite the set. Don't forget to flock() in case of truly poor timing.
Make sure your key is in the new set.
If we find the key in our cache, we extract the "n" (we'll call this the 'modulus') and "e" ('exponent') pieces from it and pass those back.
Then we decode the modulus and exponent pieces from base64URL > base64 > unencrypted (plain) text.
Create a new instance of class Crypt_RSA.
Load the numbers you just decrypted into that class as a new key, with types of Math_BigInteger so we can do math on giant numbers. (the second argument is base, so base 256 is a byte, if we are working with BIG integers, use this)
Set our hash and signature mode to match what we have from Google.
Do the verify to ensure we have a valid key.
After this it's up to you what you do with it.
Thank you once again, neubert, for the help!
The problem is that you're not base64-decoding anything relevant.
This worked for me (told me that the signature was valid):
<?php
include('Crypt/RSA.php');
$data = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjZjNzgxOTQyZDg0OWJhMmVjZGE4Y2VkYjcyZDM0MzU3ZmM5NWIzMjcifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiIxMDQ5MTQ4MTU2NTQ2LTk2YjFxcTJsNTJtODVtODB0ZHVoZHVma2RwODRtN2tuLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyNTk4NDgzNjQ2MjY1OTYxNTQwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF6cCI6IjEwNDkxNDgxNTY1NDYtdjJwZjRlbGhzOGNwcXBlcWZkMzU5am5nOWs5aW5kcTQuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJlbWFpbCI6InRlc3R1c2VydGh4QGdtYWlsLmNvbSIsImlhdCI6MTQ3NDc1NDMzMiwiZXhwIjoxNDc0NzU3OTMyLCJuYW1lIjoiVGVzdCBVc2VyIiwicGljdHVyZSI6Imh0dHBzOi8vbGg0Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tU0dldkZZRDlaWFEvQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQVBhWEhoUmtuX1hEaEhNLTEzeVMwTUtBcFNrZG1zVEdYdy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImxvY2FsZSI6ImVuIn0';
$signature = 'btukbBvhek6w14CrBVTGs8X9_IXIHZKpV1NzJ3OgbGUfmoRMirNGzZiFAgrR7COTeDJTamxRzojxxmXx6EEkQqNQcbyN8dO0PTuNt9pujQjLbFw_HBhIFJQaJSR3-tYPN-UtHGQ5JAAySsvCPapXbxyiKzTyvGYRSU65LmyNuiGxe6RQe1zHjq2ABJ4IPRqKPuFupnGRPWYyBSTPU7XQvtfhgyqA0BWZUfmCIFyDxQhvMaXNLTs01gnGVhcUDWZLi9vuUiKUlz3-aSSbwdfCMAljhBHnjpYO6341k5-qmgKkWawv8DX_nMEzntsCMCr664rP4wFEbsRB5ledM9Pc9Q';
$signature = str_replace(['-','_'], ['+','/'], $signature);
$signature = base64_decode($signature);
$n = 's1dt5wFFaYl-Bt7Yb7QgWEatLJfxwWDhbd5yvm2Z4d1PRgNVQa9kwOArQNoOJ-b-oZnXLVFsVASUXEAumGf1ip5TVCQmMBKqlchSDNuoZfoWdpCCX7jx4gNuS43pS6VqV3QDjWnoXRTHaUi5pZEbpAmWpOeG_CfmewNVwBXPFx8-mtvEdtxIrspX4ayXTViR4vHc7MhQhUxllFbocxMjJysDQuZV9wN3MI0lVtQdf52SKJwF3lhvWA9-WAEZ1q8wq-I93Sfte95RaFjDqCH--Sh-8DjhK4OvgItcEGd5QRHjdLvrayPwaDQbpMRN2n3BkVWIxKJubtRiSeWbawCklQ';
$n = str_replace(['-','_'], ['+','/'], $n);
$n = base64_decode($n);
$e = 'AQAB';
$e = base64_decode($e);
$rsa = new Crypt_RSA();
$rsa->loadKey([
'n' => new Math_BigInteger($n, 256),
'e' => new Math_BigInteger($e, 256)
]);
$rsa->setHash('sha256');
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
echo $rsa->verify($data, $signature) ?
'valid' :
'invalid';

How to use hash_hmac() with "SHA256withRSA" on PHP?

I'm trying to get PayPal Webhooks to work with my PHP app.
The problem is the hashing algorithm they send via headers, that i must use to verify if the request is valid.
When I try to use it, I get this error:
hash_hmac(): Unknown hashing algorithm: SHA256withRSA
I have tried hash_hmac using just the "sha256" algo and it worked, so I think the problem must be with the one they want me to use.
Here is the code I use to process the Webhook:
$headers = apache_request_headers();
$body = #file_get_contents('php://input');
$json = json_decode($body);
// Concatanate the reqired strings values
$sigString = $headers['PAYPAL-TRANSMISSION-ID'].'|'.$headers['PAYPAL-TRANSMISSION-TIME'].'|'.$json->id.'|'.crc32($body);
// Get the certificate file and read the key
$pub_key = openssl_pkey_get_public(file_get_contents($headers['PAYPAL-CERT-URL']));
$keyData = openssl_pkey_get_details($pub_key);
// check signature
if ($headers['PAYPAL-TRANSMISSION-SIG'] != hash_hmac($headers['PAYPAL-AUTH-ALGO'],$sigString,$keyData['key'])) {
//invalid
}
I think they are not using HMAC algorithm (symmetric), contrary to what they say in documentation, but RSA (asymmetric). So you should use openssl_verify to verify a signature. Maybe this will work:
//your code here...
// Get the certificate file and read the key
$pubKey = openssl_pkey_get_public(file_get_contents($headers['PAYPAL-CERT-URL']));
$verifyResult = openssl_verify($sigString, $headers['PAYPAL-TRANSMISSION-SIG'], $pubKey, 'sha256WithRSAEncryption');
if ($verifyResult === 0) {
throw new Exception('signature incorrect');
} elseif ($verifyResult === -1) {
throw new Exception('error checking signature');
}
//rest of the code when signature is correct...
The signature algorithm names used by PayPal may be different than those used by PHP. Refer to openssl_get_md_methods method to get valid PHP signature algorithms.
Here is the code that worked in the end:
// Get the certificate file and read the key
$pubKey = openssl_pkey_get_public(file_get_contents($headers['PAYPAL-CERT-URL']));
$details = openssl_pkey_get_details($pubKey);
$verifyResult = openssl_verify($sigString, base64_decode($headers['PAYPAL-TRANSMISSION-SIG']), $details['key'], 'sha256WithRSAEncryption');
if ($verifyResult === 0) {
throw new Exception('signature incorrect');
} elseif ($verifyResult === -1) {
throw new Exception('error checking signature');
}
//rest of the code when signature is correct...
I needed to decode the signature PayPal sent me with base64_decode() and for some reason the key worked only when I used openssl_pkey_get_details()

Signature verification of in-app billing v3 in PHP server with openssl_verify

I have seen many posts with this issue and tested many possible solutions but the verification was always false.
So, I am passing both of this parametters via POST to the PHP server:
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");<br>
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
This is the method I use:
$result = openssl_verify($responseData, base64_decode($signature),
key, OPENSSL_ALGO_SHA1);
I used the method json_decode to verify that purchaseData is correct.
I also use base64_decode for the dataSignature.
And this is how I form my key with the public key that I have with my Google Publisher account:
const KEY_PREFIX = "-----BEGIN PUBLIC KEY-----\n";<br>
const KEY_SUFFIX = '-----END PUBLIC KEY-----';
$key = self::KEY_PREFIX . chunk_split($publicKey, 64, "\n") . self::KEY_SUFFIX;
The tests are performed with an account test of a uploaded game in Google Play, so the In App Purchases are real but with no charges.
What I am missing here?
You have formatted the key but you are missing one step where you need to extract the public key from the PEM.
Here are the steps:
$key = self::KEY_PREFIX . chunk_split($publicKey, 64, "\n") . self::KEY_SUFFIX;
$key = openssl_get_publickey($key);
$result = openssl_verify($responseData, base64_decode($signature), $key, OPENSSL_ALGO_SHA1);

android in app billing v3 with php

i'm trying android in app billing v3 verifying on my remote php server.
but, it seems something is wrong at my codes.
i think this openssl_verify function is problem.
result is always failed!
i can't find what first parameter to verify with openssl_verify. actually, i 'm confuse what's reasonable format to place at first parameter :(
could you help me to solve it?
$result = openssl_verify($data["purchaseToken"], base64_decode($signature), $key); // original // failed
belows full test codes.
<?php
$responseCode = 0;
$encoded='{
"orderId":"12999763169054705758.1111111111111",
"packageName":"com.xxx.yyy",
"productId":"test__100_c",
"purchaseTime":1368455064000,
"purchaseState":0,
"purchaseToken":"tcmggamllmgqiabymvcgtfsj.AO-J1OwoOzoFd-G-....."
}';
$data = json_decode($encoded,true);
$signature = "tKdvc42ujbYfLl+3sGdl7RAUPlNv.....";
$publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2kMri6mE5+.....";
$key = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($publicKey, 64, "\n") . "-----END PUBLIC KEY-----";
$key = openssl_get_publickey($key);
if (false === $key) {
exit("error openssl_get_publickey");
}
var_dump($key);
$result = openssl_verify($data["purchaseToken"], base64_decode($signature), $key); // original // failed
//$result = openssl_verify($data, base64_decode($signature), $key); // failed
//$result = openssl_verify($encoded, base64_decode($signature), $key); // failed
//$result = openssl_verify(base64_decode($data["purchaseToken"]), base64_decode($signature), $key); // failed
//$result = openssl_verify(base64_decode($signature),$data["purchaseToken"], $key,OPENSSL_ALGO_SHA512 ); // failed
if ($result == 1) {
echo "good";
} elseif ($result == 0) {
echo "bad";
} else {
echo "error";
}
echo($result);
thanks :)
You are passing the wrong $data value into openssl_verify(). This value should be the full JSON string you get from Google Play, not the purchase token inside it. It is important that the JSON string is untouched, as even if you were to add a space or newlines to it, the signature would no longer work.
All you need to do in your code above is to change this line:
$result = openssl_verify($data["purchaseToken"], base64_decode($signature), $key);
to
$result = openssl_verify($data, base64_decode($signature), $key);
And you should get a success, assuming you're using the correct public key and the JSON purchase string is valid. I'm pretty sure your JSON string is not the original string from Google however, as the ones from Google do not contain newlines. It will be one long line of JSON text. Make sure that's what you are passing to openssl_verify().

Categories