Problems encoding Amazon Flexible Payments secret string in PHP - php

I am trying to use Amazon Payment Services, and they require me to do something like this:
Here is the complete signature so you can see I added the signature method:
$string_to_sign = "GET\n
authorize.payments-sandbox.amazon.com\n
cobranded-ui/actions/start?
SignatureMethod=HmacSHA256&SignatureVersion=2&callerKey=my_key&callerReference=YourCallerReference&paymentReason=donation&pipelineName=SingleUse&returnUrl=http%3A%2F%2Fyourwebsite.com%2Freturn.html&transactionAmount=4.0";
and then I encrypt it like below.
$encoded_string_to_sign = URLEncode(Base64_Encode(hash_hmac("sha256", $string_to_sign, 'my_secret_key')));
I do that, but then I get an error from them saying:
Caller Input Exception: The following input(s) are either invalid or absent:[signatureMethod]
Any idea what might be going wrong here?
Here is the entire code for this: (the variables are assigned values above)
<?php
$string_to_sign = 'GET
authorize.payments-sandbox.amazon.com/cobranded-ui/actions/startSignatureMethod=HmacSHA256&SignatureVersion=2&callerKey=AKIAJENBYSJCJX2IDWDQ&callerReference=YourCallerReference&paymentReason=donation&pipelineName=SingleUse&returnUrl=http%3A%2F%2Fproblemio.com&transactionAmount=4.0';
$encoded_string_to_sign = URLEncode(Base64_Encode(hash_hmac("sha256", $string_to_sign, 'my_secret_key')));
$amazon_request_sandbox = 'https://authorize.payments-sandbox.amazon.com/cobranded-ui/actions/start?SignatureVersion=2&returnUrl='.$return_url.'&paymentReason='.$payment_reason.'&callerReference=YourCallerReference&callerKey='.$my_access_key_id.'&transactionAmount=4.0&pipelineName=SingleUse&SignatureMethod=HmacSHA256&Signature='.$encoded_string_to_sign;
//echo $amazon_request_sandbox; - use this if you want to see the resulting request and paste it into the browser
header('Location: '.$amazon_request_sandbox);
?>
Thanks!!

Check if you included &SignatureMethod=HmacSHA256 on the request
This kind of errors has 3 basic natures:
Missing Keys/Values
Typos on Keys/Values
Incorrect encoding or spaces on Keys/Values
Hope that helps!
Regards

The only piece that wasn't suggested was that you need to use rawurlencode() on the transactionAmount that's part of the $string_to_sign.
Most other answers are a piece of the problem. For instance, you need to add a new line to the $string_to_sign after the GET (which you have), after the authorize.payments-sandbox.amazon.com, and after the /cobranded-ui/actions/start. You also need to set the $raw_output parameter to true in the hash_hmac() function.
I've included a complete working rewrite of your code (replace <Your_Access_Key> and <Your_Secret_Key>):
$return_url = rawurlencode('http://problemio.com');
$payment_reason = 'donation';
$transaction_amount = rawurlencode('4.0');
$secret_key = '<Your_Secret_Key>';
$my_access_key_id = '<Your_Access_Key>';
$string_to_sign = 'GET
authorize.payments-sandbox.amazon.com
/cobranded-ui/actions/start
SignatureMethod=HmacSHA256&SignatureVersion=2&callerKey=' . $my_access_key_id . '&callerReference=YourCallerReference&paymentReason=' . $payment_reason . '&pipelineName=SingleUse&returnUrl=' . $return_url . '&transactionAmount=' . $transaction_amount;
$encoded_string_to_sign = URLEncode(Base64_Encode(hash_hmac("sha256", $string_to_sign, $secret_key, true)));
$amazon_request_sandbox = 'https://authorize.payments-sandbox.amazon.com/cobranded-ui/actions/start?SignatureVersion=2&returnUrl=' . $return_url . '&paymentReason=' . $payment_reason . '&callerReference=YourCallerReference&callerKey=' . $my_access_key_id . '&transactionAmount=4.0&pipelineName=SingleUse&SignatureMethod=HmacSHA256&Signature=' . $encoded_string_to_sign;
However, I strongly suggest that you use the PHP library provided by the FPS community which can be downloaded here. I use this in production code and have never had an issue. Using the FPS library, your code would look like the following:
<?php
require_once 'CBUISingleUsePipeline.php';
require_once 'CBUIPipeline.php';
$secret_key = '<Your_Secret_Key>';
$my_access_key_id = '<Your_Access_Key>';
$return_url = 'http://problemio.com';
$transaction_amount = '4.0';
$caller_reference = '<Your_Caller_Reference>';
$payment_reason = 'donation';
$base = 'https://authorize.payments-sandbox.amazon.com/cobranded-ui/actions/start';
$pipeline = new Amazon_FPS_CBUISingleUsePipeline($my_access_key_id, $secret_key);
$pipeline->setMandatoryParameters($caller_reference, $return_url, $transaction_amount);
$pipeline->addParameter('paymentReason', $payment_reason);
$uRL = $pipeline->getURL($base);
?>

Have you set your signature method? from the AWS documentation:
You must set the SignatureMethod request parameter to either
HmacSHA256 or HmacSHA1 to indicate which signing method you're using

I don't believe you need to base64 encode the hash (after all, it's already being urlencoded) -- try removing Base64_Encode.

Your $string_to_sign variable is missing a '?' between start and SignatureMethod for your encoded Signature.
Signature version 2 is an enhanced signing method for both Amazon
Simple Pay and Amazon Flexible Payments Service.
For inbound requests (from your application to Amazon Payments), it
uses the entire request URI as the basis for the signature, with
encryption based on the unique security credentials for your account.
For outbound requests (from Amazon Payments to your application),
Amazon signs the response which you can verify using the
VerifySignature API
EDIT:
As #Jonathan Spooner mentioned already and what I use is the function varifySignature() located in
/amazon-fps-2010-08-28-php5-library/src/Amazon/FPS/Samples/Client.php
which can be downloaded here. It also has an example as to how to use it in
/amazon-fps-2010-08-28-php5-library/src/Amazon/FPS/Samples/VerifySignatureSample.php
It makes the whole process much easier. It may be worth a shot...

Have you tried this
base64_encode(hash_hmac('sha256', $Request, $AmazonSecretKey, true));
Pass a boolean to pass it as a raw output.

You're most definitely missing the last parameter for hash_hmac which has to be set true to get RFC 2104-compliant HMAC signature:
base64_encode(
hash_hmac($hash, $data, $key, true)
);
And in the complete example you're missing new lines in $string_to_sign.

Related

Reverse JWT in PHP

I had created JWT in php.I had found the way to create JWT from following link.
JWT (JSON Web Token) in PHP without using 3rd-party library. How to sign?
<?php
//build the headers
$headers = ['alg'=>'HS256','typ'=>'JWT'];
$headers_encoded = base64_encode(json_encode($headers));
//build the payload
$payload = ['sub'=>'1234567890','name'=>'John Doe', 'admin'=>true];
$payload_encoded = base64_encode(json_encode($payload));
//build the signature
$key = 'secret';
$signature = hash_hmac('SHA256',"$headers_encoded.$payload_encoded",$key,true);
$signature_encoded = base64_encode($signature);
//build and return the token
$token = "$headers_encoded.$payload_encoded.$signature_encoded";
echo $token;
?>
Now how can i authenticate it. I am sending token from Android but i want to validate that this is proper token or not. So how can i do it in code before fulfilling the request.
Should i store token in database?
And is it proper way to give security to api?
I highly recommend using a well known JWT library for this. This is cryptography, and rolling your own crypto is usually dangerous. There are a few packages around with widespread adoption that have been vetted by security professionals.
If you are going to do this manually, at least take inspiration from one of these packages to ensure that you're doing it correctly: https://github.com/firebase/php-jwt/blob/master/src/JWT.php#L69-L138
The linked code is pretty easy to follow. Essentially you're:
Decoding the token by splitting on ., base64_decodeing, and then json_decodeing.
Checking the signature of the provided JWT against one that is computed again from the decoded header and payload. The alg header property in your example will tell you what algorithm to use to check the signature.
Short solution for your example :
public function verify(string $token, string $secret): bool
{
[$headerEncoded, $bodyEncoded, $signatureEncoded] = explode('.', $token);
$signature = base64_decode($signatureEncoded);
$hash = hash_hmac('sha256', implode('.', [$headerEncoded, $bodyEncoded]), $secret, true);
return \hash_equals($signature, $hash);
}
But you sohuld encode and decode string with URL-Safe Base64.

Issues Matching WooCommerce WebHook signature on Node.JS

I am attempting to a validate the webhook transaction from WooCommerce on my Node.js website. However I cannot get the 2 strings to match.
I can see that the php signature is generated with the following code, and the source can be viewed here WooCommerce Source.
base64_encode( hash_hmac( $hash_algo, $payload, $this->get_secret(), true ) ));
I have noticed that if i turn off true on the hash_hmac, I can then get the 2 systems to create a match, however I would rather not edit the core of WooCommerce so I am wondering if there is something I am missing here?
For my Example I did edit the core and forced the payload to be the following, just so i could easily try and match the 2 systems
payload = '{"id":1,"etc":2,"host":"http:/\/localhost\/view-order\/8"}'
secret = 'welcome'
My code in Node.Js is the following.
var crypto = require('crypto');
hmac = crypto.createHmac('sha256', secret);
hmac.setEncoding('binary');
hmac.write(payload);
hmac.end();
hash = hmac.read();
result = base64.encode(hash);
console.log(result);
If I remove the url from the "host" JSON then it does work, is it something to do with the way it has been escaped?
I think it may be an issue with the way PHP and node do the SHA256 hashing. I really can't workout exactly how to solve this.
Any help would be great,
Thanks
I have run into a similar issue as you, using the code suggested here:
SHA256 webhook signature from WooCommerce never verifies
var processWebHookSignature = function (secret, body, signature) {
signatureComputed = crypto.createHmac('SHA256', secret)
.update(new Buffer(JSON.stringify(body), 'utf8'))
.digest('base64');
return ( signatureComputed === signature ) ? true : false;
}
(Where body comes from req.body).
This only started working for me when I changed the way I obtain the raw body. I got it using the bodyParser middleware:
app.use(bodyParser.json({verify:function(req,res,buf){req.rawBody=buf}}))
(As explained in: https://github.com/expressjs/body-parser/issues/83#issuecomment-80784100)
So now instead of using
new Buffer(JSON.stringify(body), 'utf8') I just use req.rawBody
I hope this solves your problems too.

How to verify Paypal webhook signature in PHP?

I'm not very knowledgeable in SSL and certificates. I used the post
"How to use hash_hmac() with "SHA256withRSA" on PHP?" to see if I can get webhooks with PayPal working.
The issue I am have is I am getting the following error after calling openssl_verify() and a return result of (0):
OpenSSL error openssl_verify error:04091068:rsa routines:INT_RSA_VERIFY:bad signature
I've tried to solve this, but documentation on errors and the functions around the web is minimal to none.
My current code looks like this:
// get the header post to my php file by PayPal
$headers = apache_request_headers();
// get the body post to me php file by PayPal
$body = #file_get_contents('php://input');
$json = json_decode($body);
// TransmissionId|TransmissionTimeStamp|WebhookId|CRC32 as per PayPal documentation
$sigString = $headers['Paypal-Transmission-Id'].'|'.$headers['Paypal-Transmission-Time'].'|'.$json->id.'|'.crc32($body);
// $headers['Paypal-Cert-Url'] contains the "-----BEGIN CERTIFICATE---MIIHmjCCBoKgAwIBAgIQDB8 ... -----END CERTIFICATE-----"
$pubKey = openssl_pkey_get_public(file_get_contents($headers['Paypal-Cert-Url']));
// and this is the call to verify that returns result (0)
$verifyResult = openssl_verify($sigString, base64_decode($headers['Paypal-Transmission-Sig']), $pubKey, 'sha256WithRSAEncryption');
Only different from the reference code I used, is that I do not use openssl_pkey_get_details($pubKey) because I will get below error in addition to the existing signature error:
OpenSSL error openssl_verify error:0906D06C:PEM routines:PEM_read_bio:no start line
OpenSSL error openssl_verify error:04091068:rsa routines:INT_RSA_VERIFY:bad signature
Also I've tried a variation by not using base64_decode() on the header but that would get the same return result (0) with error stating:
OpenSSL error openssl_verify error:04091077:rsa routines:INT_RSA_VERIFY:wrong signature length
What is wrong with the signature?
You may want to use this piece of code:
$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');
}
The formula is <transmissionId>|<timeStamp>|<webhookId>|<crc32> not <transmissionId>|<timeStamp>|<eventId>|<crc32>. Also note that Webhook simulator events can't be verified.
This may not be exactly what you were looking for, but an alternative to manually validating the signature with Open SSL could be to use the PayPal PHP Restful API.
The PayPal Restful API exposes an endpoint that allows you to validate webhook: /v1/notifications/verify-webhook-signature
The PayPal-PHP-SDK provides a VerifyWebhookSignature class that make it easy to make calls to that end point.
They also have a Sample Script illustrating how to use VerifyWebhookSignature class.
As #JUBEI mentioned, you need to get the WEBHOOK_ID from your PayPal account and NOT from the headers you've received, remember the first time you've registered the webhook event, you must find your webhook ID right there.
Plus, make sure to use OPENSSL_ALGO_SHA256 instead of: 'sha256WithRSAEncryption', refer to: https://www.php.net/manual/en/openssl.signature-algos.php

Amazon SimpleDB PuttAttributes

I am new to both PHP and Amazon SimpleDB. I am trying to figure out how to update values in SimpleDB. I created a signature which returns a value so I believe that is working. When I try to follow Amazon's PutAttribute example, my application breaks. Any hints or ideas of what I could be doing wrong would be greatly appreciated.
$item_name = htmlspecialchars($_POST["item_name"]);
$timestamp = gmdate('c');
$secretkey = 'mysecretkey';
$accesskey = 'myaccesskey';
$message = "
https://sdb.amazonaws.com/
?Action=PutAttributes
&Attribute.1.Name=body_type
&Attribute.1.Value=02
&Attribute.1.Replace=true
&AWSAccessKeyId=[my access key]
&DomainName=FILE_LOG_DEV
&ItemName={$item_name}
&SignatureVersion=2
&SignatureMethod=HmacSHA256
&Timestamp={$timestamp}
&Version=2009-04-15
";
$signature = base64_encode(hash_hmac('sha1', $message, $secretkey, true));
https://sdb.amazonaws.com/
?Action=PutAttributes
&Attribute.1.Name=body_type
&Attribute.1.Value=02
&Attribute.1.Replace=true
&AWSAccessKeyId=[my access key]
&DomainName=FILE_LOG_DEV
&ItemName=$item_name
&SignatureVersion=2
&SignatureMethod=HmacSHA1
&Timestamp=$timestamp
&Version=2009-04-15
&Signature=$signature
You can use AWS PHP SDK (link) that will help you to interact with Amazon SimpleDB Database Service in easy and friendly manner. AWS PHP SDK will internally manage your creating signature and time stamp. So you need to only focus on parameters pass to your method and result what you get from that method. Have a look on example. Here it will take an array as argument and returns a response Model object.

Problem with RSA-encrypting a string in PHP and passing it to .NET service

I need to call a web service that requires a login url containing an RSA encrypted, base_64 encoded and url-encoded login data. I don't have a formal php training, but even for me it seems like an easy task, however when calling the service I get an 'Invalid Format' response. What am I doing wrong and is there another way to come up with the encrypted string? Code example below. Thank you for your help!
require 'rsa.php'; // Uses rsa.php from http://www.edsko.net/misc/ for encryption.
$message = '?id=112233&param1=hello&email=hello#world.net&name=Name';
$keyLength = '2048';
$exponent = '65537';
$modulus = '837366556729991345239927764652........';
$encryptedData = rsa_encrypt($message, $exponent, $modulus, $keyLength);
$data = urlencode(base64_encode($encryptedData));
$loginurl = 'http://www.somedomain.com/LoginWB.aspx?Id=9876&Data='.$data;
echo '<iframe src="'.$loginurl.'" width="570px" height="800px">';
echo '</iframe>';
The encryption looks correct to me. Are you sure the server is not complaining about the '?' in the $message?
If you care about performance, you should use openssl extensions. The public key operations are very slow in PHP.

Categories