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';
Related
I'm trying to encrypt a message using phpseclib. Below is the method that encrypts it:
function RSAEncrypt($data, $publicKey)
{
$rsa = new \Crypt_RSA();
$rsa->loadKey($publicKey);
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
$encryptedData = $rsa->encrypt($data);
$encodedData = base64_encode($encryptedData);
return $encodedData;
}
the code that encrypt message
$client_key = 123456789;
$random_str = rand();
$aes_password = $client_key.'-'.$random_str;
$public_key = file_get_contents('keys/public.xml');
$ecrypted_password = RSAEncrypt($aes_password, $public_key);
but no matter the message the $aes_password that i passed , the $ecrypted_password output is always
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE= ◀"
I tried to decrypt it using the private key to see if its valid anyways but i always gets Decryption Error so i figured maybe the error is with the encryption
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);
help me please with implementing semantic code from manual about SCRAM-SHA-1 authorization in XMPP server. So, we got:
clientFinalMessageBare = "c=biws,r=" .. serverNonce
saltedPassword = PBKDF2-SHA-1(normalizedPassword, salt, i)
clientKey = HMAC-SHA-1(saltedPassword, "Client Key")
storedKey = SHA-1(clientKey)
authMessage = initialMessage .. "," .. serverFirstMessage .. "," .. clientFinalMessageBare
clientSignature = HMAC-SHA-1(storedKey, authMessage)
clientProof = clientKey XOR clientSignature
clientFinalMessage = clientFinalMessageBare .. ",p=" .. base64(clientProof)
My PHP code:
$cfmb = 'c=biws,r='.$salt;
$saltpass = hash_pbkdf2('sha1', 'IDoMdGuFE9S0', $ps, $iter);
//hash_pbkdf2('sha1', 'IDoMdGuFE9S0', $salt, $iter, 0, true); maybe like that???
$ckey = hash_hmac('sha1', $saltpass, 'Client Key');
$sckey = sha1($ckey);
$authmsg = $im.','.$chal.','.$cfmb;
$csign = hash_hmac('sha1', $sckey, $authmsg);
$cproof = bin2hex(pack('H*',$ckey) ^ pack('H*',$csign));
$cfm = $cfmb.',p='.base64_encode($cproof);
Somewhere error (maybe ALL big error ;)) and I very need your help for correcting my code, maybe I am use wrong functions, or arguments in wrong positions? Because result - fail, server sends me that:
"The response provided by the client doesn't match the one we calculated."
PS: Sorry for my bad English ;)
First of all, it's very confusing to use $salt for the serverNonce and $ps for the salt.
But more importantly, you should take some care to keep track of whether the functions you use return binary data or hexadecimal encoded strings. hash_pbkdf2, sha1 and hash_hmac by default return hexadecimal encoded strings. You call pack('H*', ...) to decode them for the $cproof, but not when you calculate $ckey and $csign.
A much easier way is to compute binary data directly, by always passing $raw_data = TRUE:
$cfmb = 'c=biws,r='.$salt;
$saltpass = hash_pbkdf2('sha1', 'IDoMdGuFE9S0', $ps, $iter, 0, TRUE);
$ckey = hash_hmac('sha1', 'Client Key', $saltpass, TRUE);
$sckey = sha1($ckey, TRUE);
$authmsg = $im.','.$chal.','.$cfmb;
$csign = hash_hmac('sha1', $authmsg, $sckey, TRUE);
$cproof = $ckey ^ $csign;
$cfm = $cfmb.',p='.base64_encode($cproof);
Also, your hash_hmac calls were the wrong way around: first the data, then the key.
hello fellow developers,
I’m facing an issue with the load callback (and the uninstall callback by extension).
I’m trying to verify the requests authenticity following the algorithm described in the documentation. https://developer.bigcommerce.com/apps/load#signed-payload
I am able to decode the json string and the data is correct, but the signatures never match. I made sure to use the right client secret and tried out different encoding/decoding scenarios with no luck.
An other concern is with the snippet of code (PHP) they provide in example (and in their sample app). They seem to return null when the signatures match and the decoded data when they don’t… (try secureCompare())
Meaning that the security test would pass every time, since in all my attempts the signatures didn’t match.
Am I missing something here ?
Edit: Here is the example in the doc. I can't really give you sample data as the client secret is to remain secret...
function verify($signedRequest, $clientSecret)
{
list($payload, $encodedSignature) = explode('.', $signedRequest, 2);
// decode the data
$signature = base64_decode($encodedSignature);
$data = json_decode(base64_decode($payload), true);
// confirm the signature
$expectedSignature = hash_hmac('sha256', $payload, $clientSecret, $raw = true);
if (secureCompare($signature, $expectedSignature)) {
error_log('Bad Signed JSON signature!');
return null;
}
return $data;
}
function secureCompare($str1, $str2)
{
$res = $str1 ^ $str2;
$ret = strlen($str1) ^ strlen($str2); //not the same length, then fail ($ret != 0)
for($i = strlen($res) - 1; $i >= 0; $i--) {
$ret += ord($res[$i]);
}
return !$ret;
}
You're not missing anything, and it's not a clock sync issue - the 28 lines of sample code provided both here and here has some pretty critical flaws:
The sample code does a hash_hmac of the raw base64-encoded JSON, instead of the base64-decoded JSON. (The hash provided to you by the BigCommerce API is really a hash of the base64-decoded JSON).
Since hash_hmac is called with $raw=true, this means the two strings will always be vastly different: one is raw binary, and the other is hexits.
Bad check of secureCompare logic. The if (secureCompare... part of the verify function expects opposite behavior from the secureCompare function. If the secureCompare function returns true when the strings match, why are we calling error_log?
Put all three of these issues together, and you end up with code that appears to work, but is actually silently failing. If you use the sample code, you're likely allowing any and all "signed" requests to be processed by your application!
Here's my corrected implementation of the verify function:
<?php
function verifySignedRequest($signedRequest, $clientSecret)
{
list($encodedData, $encodedSignature) = explode('.', $signedRequest, 2);
// decode the data
$signature = base64_decode($encodedSignature);
$jsonStr = base64_decode($encodedData);
$data = json_decode($jsonStr, true);
// confirm the signature
$expectedSignature = hash_hmac('sha256', $jsonStr, $clientSecret, $raw = false);
if (!hash_equals($expectedSignature, $signature)) {
error_log('Bad signed request from BigCommerce!');
return null;
}
return $data;
}
Usually, I use openssl_encrypt to encrypt simple string with AES in PHP, and it works pretty well.
Now I need to encrypt files with AES-256-CTR mode, but the only way to do this is to file_get_contents the entire content of the file and then send it to the openssl_encrypt function to encrypt the actual file data. The problem is this method is very "poor" because of the critical waste of memory.
1) Is there a way to work with chunked data with PHP OpenSSL ?
For example:
<?php
// ...
$f = fopen('large.iso','r');
while(feof($f)){
$chunk = fread($f,16);
$cipher = openssl_encrypt(...$chunk...);
// ... code ...
}
// ... more code ...
?>
2) openssl_encrypt official documentation is not published yet. Does someone could clarify the meaning of the parameters of the function for use with AES-CTR mode? Does the counter is handled automatically? Is it necessary to apply a manual XOR the data returned by the function?
Note: It is a professional project so I don't want to use phpseclib or others' "anonymous" libraries, nor do I don't want to use the command line as well.
Looks like for php it's not possible to use aes-256-ctr without temporary file.
But for next chiper types:
OPENSSL_CIPHER_RC2_40
OPENSSL_CIPHER_RC2_128
OPENSSL_CIPHER_RC2_64
OPENSSL_CIPHER_DES
OPENSSL_CIPHER_3DES
OPENSSL_CIPHER_AES_128_CBC
OPENSSL_CIPHER_AES_192_CBC
OPENSSL_CIPHER_AES_256_CBC
you can use generating key on the fly:
$res = openssl_pkey_new('chiper args here');
openssl_pkey_export($res, $private_key);
$public_key = openssl_pkey_get_details($res);
$public_key = $public_key["key"];
Then encrypt:
$crypted_text = openssl_get_privatekey($private_key,'your data');
And decrypt:
openssl_public_decrypt($crypted_text,$decrypted_text,$public_key);
So if you don't want to use files, may be switching to OPENSSL_CIPHER_AES_256_CBC will help you?
1) It should be something like this:
function strtohex($x) {
$s = '';
foreach (str_split($x) as $c){
$s.=sprintf("%02X", ord($c));
}
return($s);
}
$method = "aes-256-ctr"; //aes-256-cbc
echo "Selected method: ".$method."<br /><br />";
$textToEncrypt = "My chunk of data";
$iv = "1234567890123456";
$pass = 'some_pass';
$dec_iv = strtohex($iv);
$key = strtohex($pass);
$enc_data = openssl_encrypt($textToEncrypt, $method, $pass, true, $iv);
echo "Encrypted message (openssl): ".$enc_data."<br />";
$dec_data = openssl_decrypt($enc_data, $method, $pass, OPENSSL_RAW_DATA, $iv);
echo "Decrypted message (openssl): ".$dec_data."<br />";
For CTR $iv should be unique for each chunk or your data can be broken.
2) I know only abot difference betwen CBC and CTR:
For CBC, the IV must be random, but not unique. It also must not be known.
For CTR, the IV must be unique and not known, but does not need to be random.