I have been trying to convert a php script to a python3 script and have been failing to send through a successful signature after troubleshooting through the issue.
The below PHP example code successfully goes through so I've copied the sent timestamp and signature to try and replicate it in my python3 code.
PHP Code
<?
$nonce = "2015-10-26 04:53:49 EDT";
$secret = "mkdaklmdflfkdsmaflkdmsfdkasmfdsmflks";
$req = array();
$req['t'] = $nonce;
$req['secret'] = $secret;
$post_data = json_encode($req);
$post_data = bin2hex($post_data);
$sign = hash_hmac("sha256", $post_data, $secret);
print($sign);
?>
Python 3 Code
import json
import hmac
import binascii
import hashlib
nonce = "2015-10-26 04:53:49 EDT"
secret = "mkdaklmdflfkdsmaflkdmsfdkasmfdsmflks"
payload = {}
payload["t"] = nonce
payload["secret"] = secret
payload_json = json.dumps(payload)
post_data = binascii.b2a_hex(payload_json.encode('utf-8'))
sign = hmac.new(bytes(secret, "utf-8"), post_data, hashlib.sha256).hexdigest()
print(sign)
Based on the successful request using PHP my signature should be:
'c30ddc5878ff7b1a9b1c9078ccbdc38afef25ed510681a3d3bfc56f6c9e2f26a'
Instead I'm getting:
'f9fc8749389137252e7f207468d88a5c871110403ec533496720efd715541ec2'
Any help troubleshooting this issue would be greatly appreciated as I'm new to Python.
Your problem is that by default, Python inserts spaces into the JSON string. Output of the PHP json_encode($req):
{"t":"2015-10-26 04:53:49 EDT","secret":"mkdaklmdflfkdsmaflkdmsfdkasmfdsmflks"}
Output of the Python json.dumps(payload):
{"t": "2015-10-26 04:53:49 EDT", "secret": "mkdaklmdflfkdsmaflkdmsfdkasmfdsmflks"}
You can fix this by specifying separators, i.e.:
json.dumps(payload, separators=(',', ':'))
Also, you can control the order of the elements by using OrderedDict instead of a plain dictionary:
from collections import OrderedDict
payload = OrderedDict()
Related
I have a work to do and I need to login to the webpage, and extract content from it.
The query has to be made of a username and an access key.
The problem is that I don't really know how to make a query with thos 2 elements, and in PHP.
So, I have found this code have this code :
$ak = "accesskey";
$username = 'admin';
$password = '123';
$remote_url = 'http://192.168.1.78/index.php';
// Create a stream
$opts = array(
'http'=>array(
'method'=>"GET",
'header' => "Authorization: Basic " . base64_encode("$username:$ak")
)
);
$context = stream_context_create($opts);
// Open the file using the HTTP headers set above
$file = file_get_contents($remote_url, false, $context);
print($file);
But the output is simply the Webpage code, from the <!DOCTYPE html><html> to </html>
According to the webpage REST API, the result that I need to get is this :
{
success: true,
result: json_result
}
Any idea why it doesn't work ?
PS : here is the API documentation : https://www.vtiger.com/docs/rest-api-for-vtiger#/Authentication
The vTiger API (in 7.2 at least) has an odd combination of parameter and form POST payloads. Here is the Python script I used to get started. Note that it uses param in the getchallenge hit but data in the login hit:
import requests
from hashlib import md5
import urllib3
urllib3.disable_warnings() # shut up whining about that security stuff
encoding = 'ascii'
userName = 'Michael'
userToken = (access key from my preferences page)
APIURL='https://our.crm.instance/webservice.php'
session = requests.Session()
def getSession(session, name, token):
response = session.get(APIURL, params={'operation':'getchallenge', 'username':userName}, verify=False)
token_key = response.json()['result']['token']
combined = token_key + userToken
accessKey = md5(combined.encode(encoding)).hexdigest()
response1 = session.post(APIURL,
data={'operation':'login', 'username':userName, 'accessKey':accessKey},
verify=False)
return response1.json()['result']['sessionName']
sessionToken = getSession(session, userName, userToken)
types = session.get(APIURL, params={'operation':'listtypes', 'sessionName':sessionToken})
print(types.json())
Recently I have used same code on PHP and it's working fine but when I tried Node Js, it's not working for me. Please check once:
PHP
$signature = $ACCID . "POST" . strtolower(urlencode($url)).$requestContentBase64String;
$hmacsignature = base64_encode(hash_hmac("sha256", $signature, base64_decode($APIKey), true));
NODE CODE :
var signature = ACCID+"POST"+encodeURI(url).toLowerCase()+requestContentBase64String;
var hmacsignature = base64.encode(crypto.createHmac('sha256', APIKey).update(signature).digest('base64'))
Please check what's wrong in this code.
Your second line has two mistakes:
PHP APIKey is BASE64_DECODE(APIKey) while in Node.js code it is just APIKey
PHP hmacsignature is BASE64(HEX(HMAC)) while in Node.js code it is BASE64(BASE64(HMAC))
Try this:
var hmacsignature = crypto.createHmac('sha256', Buffer.from(APIKey, 'base64')).update(signature).digest('base64')
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';
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;
}
I'm looking at building a REST API in Symfony2, and in their Custom Authentication Provider they show how to build a WSSE authentication system, which should be fine for what I need to do. I'm going to start off by building and testing the API through cURL, so I need to be able to quickly generate the headers. I found a JS generator that showed the headers I would need.
From what I read, the Password Digest should be a base64 encoded SHA1 of the nonce, timestamp, and user password concatenated together in that order. I started with the following data:
$nonce = '4c5625ec7af5bdff';
$timestamp = '2013-04-03T04:46:19Z';
$password = 'mypass';
and generated the digest:
$digest = base64_encode(sha1($nonce.$timestamp.$password));
What I don't understand is that the $digest variable is now set to YTgxMDUzOWQzMDBiZmU1MmI2NWQ0YjYwNDc3ZmY5OWI3MmVlZTQyNA==, but the sample PasswordDigest from the JS generator comes up as qBBTnTAL/lK2XUtgR3/5m3Lu5CQ=. I must be missing a step somewhere, but I'm not sure what it is.
Looks like I need to use the binary SHA1 result, not the hex representation. My digest should look like this:
$digest = base64_encode(sha1($nonce.$timestamp.$password, true));
I know it's an old post, just tumbled on this post, recently I have developed API in symfony2 with WSSE authentication and this is how I generated full WSSE header with the help of below function:
public static function getWsseHeader($username, $apikey, $created, $nonce)
{
$digest = sha1($nonce.$created.$apikey, true);
return sprintf(
'X-WSSE: UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
$username,
base64_encode($digest),
base64_encode($nonce),
$created
);
}
//you can use this php code to generate the Digest password
<?php
date_default_timezone_set('UTC');
$t = microtime(true);
$micro = sprintf("%03d",($t - floor($t)) * 1000);
$date = new DateTime( date('Y-m-d H:i:s.'.$micro) );
echo $timestamp = $date->format("Y-m-d\TH:i:s").$micro . 'Z';
$nonce = mt_rand(10000000, 99999999);
echo $nounce = base64_encode($nonce);//we have to decode the nonce and then apply the formula on it and in xml we have to send the encoded nonce
$password = "AMADEUS"; //clear password
echo $passSHA = base64_encode(sha1($nonce . $timestamp . sha1($password, true), true));
?>