How to properly generate a WSSE Password Digest in PHP - php

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

Related

How to perform HMAC-SHA1 with Base64 Encode?

I'm trying to setup an app to sign with my URLs so they may authenticate but I can't seem to figure out how to replicate the code that I'm trying from the following page: https://help.sendowl.com/help/signed-urls#example
order_id=12345&buyer_name=Test+Man&buyer_email=test%40test.com&product_id=123&signature=QpIEZjEmEMZV%2FHYtinoOj5bqAFw%3D
buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123
buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t
publicStr&t0ps3cr3t
This is the steps:
First order the parameters (removing the signature) and unescape
them:
Next append your Signing secret:
Generate the key to sign with:
Perform the HMAC-SHA1 digest with Base 64 encode: QpIEZjEmEMZV/HYtinoOj5bqAFw=
The following is what I tried but end up not getting the same result:
$signKey = "t0ps3cr3t";
$signData = "buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t";
$passData = hash_hmac("sha1", $signData, base64_decode(strtr($signKey)), true);
$passData = base64_encode($passData);
echo $passData;
I keep getting x8NXmAmkNBPYCXwtj65mdVJ8lPc=
I was able to replicate with the following: took me a bit to figure out something so simple.. been coding for 11 hours straight.
Thanks.
$data = "buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t";
$key = "publicStr&t0ps3cr3t";
$pass1 = hash_hmac('sha1', $data, $key, true);
$pass = base64_encode($pass1);
echo $pass;
$pass will return "QpIEZjEmEMZV/HYtinoOj5bqAFw=", the correct value.
$current_timestamp = Carbon::now()->timestamp;
$signstring = "z001-line-anime-gif." . $current_timestamp . ".aaaabbbbccccdddd";
$secret = 'STG-7f*(:hsM-1_eQ_ZD175QgEoJhI$:oR.zEQ<z';
$sig = hash_hmac('sha1', $signstring, $secret);
$signature = hex2bin($sig);
$signature = base64_encode($signature);
return $signature;

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 generate a signature for OAuth

I'm currently attempting to generate a signature to make API calls to quickbooks online, however I keep getting authentication errors. I'm sure the signature portion is where I'm going wrong. Is this incorrect:
//method to generate signature
//$this->method = "GET"
//QBO_SANDBOX_URL = 'https://some_url.com/'
//$this->_query = 'something=something'
public function generate_signature()
{
$base = $this->_method.'&'.rawurlencode($this->_url.QBO_SANDBOX_URL.'v3/company/'.$this->_realm_id).'&'
.rawurlencode("oauth_consumer_key=".rawurlencode($this->_consumer_key).'&'
.'&oauth_nonce='.rawurlencode('34604g54654y456546')
.'&oauth_signature_method='.rawurlencode('HMAC-SHA1')
.'&oauth_timestamp='.rawurlencode(time())
.'&oauth_token='.rawurlencode($this->_auth_token)
.'&oauth_version='.rawurlencode('1.0')
.'&'.rawurlencode($this->_query));
$key = rawurlencode($this->_consumer_secret.'&'.$this->_token_secret);
$this->_signature = base64_encode(hash_hmac("sha1", $base, $key, true));
}
Now when I go to send my request, here are the headers:
$this->_headers = array(
'Authorization: '.urlencode('OAuth oauth_token="'.$this->_auth_token.'",oauth_nonce="ea9ec8429b68d6b77cd5600adbbb0456",oauth_consumer_key="'.$this->_consumer_key.'",oauth_signature_method="HMAC-SHA1", oauth_timestamp="'.time().'", oauth_version ="1.0"oauth_signature="'.$this->_signature.'"').''
);
I get a 401 Authorization response. Am I signing incorrectly?
EDIT: All fields not included here (i.e $this->_auth_token) are set.
For anyone that might use this as a basis for their own integration, there is one other issue with the code originally posted:
$key = rawurlencode($this->_consumer_secret.'&'.$this->_token_secret);
should be
$key = rawurlencode($this->_consumer_secret).'&'.rawurlencode($this->_token_secret);
This issue was in the base string:
.rawurlencode("oauth_consumer_key=".rawurlencode($this->_consumer_key).'&'
.'&oauth_nonce='.rawurlencode('34604g54654y456546')
The & after the consumer key and once again before the oauth_nonce.
In the signature creation I think it lacks the call to rawurlencode():
$this->_signature = rawurlencode(base64_encode(hash_hmac("sha1", $base, $key, true)));
instead :
$this->_signature = base64_encode(hash_hmac("sha1", $base, $key, true));

SCRAM-SHA-1 auth by PHP

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.

Amazon S3 Signed URLs to change content type

We have a large number of videos/audo/media hosted on a custom domain on S3 and have created a set of functions in order to sign the URLs and allow them to be both streamable and downloadable. The problem is that the signed URL of course never works. The error is:
The request signature we calculated does not match the signature you provided. Check your key and signing method.
Of course if we take the bytecode returned from this page and input it into the Amazon S3 Signature Tester and grab the bytecode from there it works just fine. Even if the string to sign from our function as well as the decoded byte code in the Signature Tester are identical, it never works.
It's called via a small block of PHP code:
$headers = createS3parameters($expiry, $file_type);
$request = preg_replace("/^.*?:\/\/.*\//", "/", $bucketurl);
$signature = signRequest($request, $expiry, $s3secret, $headers, "GET", $file_type);
$signed_request = "$bucketurl?AWSAccessKeyId=$s3key&Expires=$expiry&$headers&Signature=$signature";
This is the function which actually signs it.
function signRequest($request, $expiration, $s3secret, $headers = '', $type = 'GET', $content_type = 'default')
{
if ($expiration == 0 || $expiration == null)
{
$expiration = time() + 315576000; // 10 years (never)
}
if (strcmp($content_type, 'default') == 0)
{
$content_type = "";
}
// S3 tester spits out this format
/*$string = "$type\n".
"\n\n".
"$expiration\n".
"/mybucket$request?$headers";*/
$string = "$type\n".
"\n".
"$content_type\n".
"$expiration\n".
"$headers\n".
"$request";
// must be in UTF8 format
$string = utf8_encode(trim($string));
// encode to binary hash using sha1. require S3 bucket secret key
$hash = hash_hmac("sha1",$string, $s3secret,false);
// sha1 hash must be base64 encoded
$hash = base64_encode($hash);
// base64 encoded sha1 hash must be urlencoded
$signature = rawurlencode($hash);
return $signature;
}
Which then creates a URL such as:
http://mybucket.s3.amazonaws.com/My_Boring_Video.wmv?AWSAccessKeyId=AKIAIEXAMPLE6GA3WYQ&Expires=1344160808&response-content-type=application/force-download&response-expires=1344160808&Signature=OTIxOTI0YjNjMTA1NjMyNmJjYTk0MGE2YWJkMmI5OWQ3MGM2ZGY0MQ%3D%3D
Which unfortunately doesn't work. Is there an obvious problem here I've been staring at far too long to properly figure out?
UPDATE: Specs are specs, but are only help if they match actual practice.
Amazon's S3 specs say the signature should be formed as the following:
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) );
StringToSign = HTTP-VERB + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
Expires + "\n" +
CanonicalizedAmzHeaders +
CanonicalizedResource;
HOWEVER the actual request needed looks like this:
StringToSign = HTTP-VERB + "\n" +
"\n" +
"\n" +
Expires + "\n" +
Bucket + CanonicalizedResource + "?" + CanonicalizedAmzHeaders;
Strangely enough, in PHP you also can't seem to do this:
$string = "$type\n".
"\n".
"\n".
"$expiration\n".
"/$bucket$request?$headers";
It changes the signature and ends up rejected so must all be on a single line. I haven't gone as far as checking whether this is a bug in our particular version of PHP or just a general PHP bug (or maybe intended functionality??!). You must also include the name of your bucket even if you are using the vanity URLs available such as mybucket.s3.amazonaws.com or mybucket.mydomain.com. The documentation doesn't specify what you can or can't do and I made the assumption that since we are using an S3 based vanity URL it (S3) would pick up on the domain name and translate it to the bucket.
I ended up changing my function to be the following:
function signRequest($bucket, $request, $expiration, $s3secret, $headers = '', $type = 'GET', $content_type = 'default')
{
if ($expiration == 0 || $expiration == null)
{
$expiration = time() + 315576000; // 10 years (never)
}
if (strcmp($content_type, 'default') == 0)
{
$content_type = "";
}
$headers = trim($headers, '&');
// This is the spec:
/*$string = "$type\n".
"\n".
"$content_type\n".
"$expiration\n".
"$headers\n".
"$bucket$request";*/
// but it will only work as this
$string = "$type\n\n\n$expiration\n/$bucket$request?$headers";
// this could be a single line of code but left otherwise for readability
// must be in UTF8 format
$string = utf8_encode(trim($string));
// encode to binary hash using sha1. require S3 bucket secret key
$hash = hash_hmac("sha1",$string, $s3secret,true);
// sha1 hash must be base64 encoded
$hash = base64_encode($hash);
// base64 encoded sha1 hash must be urlencoded
$signature = urlencode($hash);
return $signature;
}
Hopefully someone else finds this useful as well.
UPDATE (20180109): Adding in the function that calls this (aka let's make this blatently simple).
It helps understanding of what to pass to the signRequest() function.
private function genS3QueryString($bucketurl)
{
$file_type = 'application/force-download';
$expiry = '1831014000'; //Sun, Jan 09 2028 0700 UTC
$headers = '&response-content-type='.$file_type.'&response-expires='.$expiry;
$bucket = preg_replace("/^.*?:\/\/(.*)\.s3\.amazonaws\.com\/.*/", "$1", $bucketurl);
$request = preg_replace("/^.*?:\/\/.*\//", "/", $bucketurl);
$signature = $this->signRequest($bucket, $request, $expiry, S3_SECRET_KEY, $headers, 'GET', $file_type);
$signed_request = '?AWSAccessKeyId='.S3_KEY.'&Expires='.$expiry.$headers.'&Signature='.$signature;
return $signed_request;
}
You'll note however the first function only generates the encrypted signature section required to attach to a GET request for AWS. As per the docs (see the link far above) more is required for the request, as is formed from the second function just above.
The expiry time I believe can be a rolling expiry time if you so desire but should be sufficiently in the future that the asset becomes outdated and replaced long before the actual expiry time. An arbitrary time was chosen to sufficiently outlast the current version of the website.
The second function only requires the bucket url of the protected asset. The desired response type could be added to the call or simply changed so that asset (in this case just non-displayable) documents are downloaded as a different type.
The signed_request that is then returned must then be appended back onto the bucketurl for a working URI to request from a protected S3 asset.

Categories