Issues Matching WooCommerce WebHook signature on Node.JS - php

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.

Related

PHP OpenSSL cannot read public key in PEM format

I have a NodeJS application generating JSON Web Tokens with the PS256 algorithm. I want to try and verify the signatures in these tokens in a PHP application.
So far I've got the following:
My JWT:
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTBiYjllYS00YTg0LTQ1ZTMtOTg5My0wYzNhNDYxZmQzMGUiLCJpYXQiOjE2MDU4OTI5NjcsImV4cCI6MTYwNjQ5Nzc2NywiYXVkIjoiNzBiYzcxMTQ1MWM2NDBjOTVlZjgzYjdhNDliMWE0MWIiLCJpc3MiOiIyM2ZhYTRiNC0wNmVlLTRlNGEtYTVjZC05NjJmOTRhMjEzYmYiLCJqdGkiOiI1MTNiYjczZC0zOTY3LTQxYzUtODMwOS00Yjc1ZDI4ZGU3NTIifQ.kLtaSYKyhqzx7Dc7UIz7tqU8TsXabRLxGiaqw21lgCcuf_eBvpiLkFOuXpUs-V8XQunQg8jV-bKlKUIb0pLvipjhRP50IwKDClQgNtIwn4yyX5RyDNGJur0qHNnkHMLaF11NsXGPyhvh-6ogSZjWgyZnkQJkXpz4jggBetwqz1hnicapGfNb6C-UdRcOLyCaiMD4OmvniFVCY6YoKlC6eHdwxsgHAxOSgD1QKiiQX_yAe39ja_axZD2Ii3QaNgO0WXzfWMbqRg_yl0y3kjQFys9iXGvQ1JIKDMLffR3rKVL5PgKSU3e472xcPKf6PNSJzphPi1G_xH2gqg1VVXo3Lg
Decoded:
Header:
(
[alg] => PS256
[typ] => JWT
)
Body:
(
[sub] => 010bb9ea-4a84-45e3-9893-0c3a461fd30e
[iat] => 1605892967
[exp] => 1606497767
[aud] => 70bc711451c640c95ef83b7a49b1a41b
[iss] => 23faa4b4-06ee-4e4a-a5cd-962f94a213bf
[jti] => 513bb73d-3967-41c5-8309-4b75d28de752
)
sub is a GUID user ID (we utilize GUIDs so that if a user's ID is leaked no information can be extrapolated, like the number of users in our system or when a user signed up)
iat is the epoch time that the token was issued (UTC)
exp is the epoch time that the token will expire (UTC)
aud doesn't conform to the JWT spec. I abused this claim to mitigate the effects of stolen tokens. It's the MD5 hash of data sent with every client request that would be difficult for someone to guess. So if someone were to steal this token and use it without sending the appropriate passphrase, the token would be automatically revoked
iss also doesn't conform to the JWT spec. I abused this claim to list the ID of the key used for signing the JWT. This way I can rotate my public-private key pair and know which key to use when validating signatures
jti is a GUID uniquely identifying the JWT. Compared against an in-memory store of revoked tokens
I went with the PS256 algorithm over RS256 because I read on a blog post that it's more secure. Honestly I don't know the difference.
I went with the PS256 algorithm over ES256 because upon testing I found that while ES256 generated smaller signatures (and therefore smaller tokens), it took about 3x longer to compute. My goal is to make this app as scalable as possible, so long computation time is to be avoided.
My public key:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA0wO7By66n38BCOOPqxnj78gj8jMKACO5APe+M9wbwemOoipeC9DR
CGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2klyKF7dGQwecgf3So7bve8vvb+vD8C6l
oqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawym+OcE/0pzWlNV9asowIFWj/IXgDVKCFQ
nj164UFlxW1ITqLOQK1WlxqHAIoh20RzpeJTlX9PYx3DDja1Pw7TPomHChMeRNsw
Z7zJiavYrBCTvYE+tm7JrPfbIfc1a9fCY3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCm
QhPE4jcDINUds8bHu2on5NU5VmwHjQ46xwIDAQAB
-----END RSA PUBLIC KEY-----
Using jsonwebtoken for NodeJS I can verify this token and authorize requests made using it. So all of the data seems good, the key works, and the math checks out.
However I've run into two problems when trying to verify the token in PHP:
1. The public key doesn't seem to be valid?
$key = openssl_pkey_get_public($pem);
print_r($key);
die();
This code prints out "false" - suggesting that the key could not be read from the PEM text posted above. Googling around I found this comment in the PHP manual which provided a solution. I did as instructed (removed new-lines from my key, prepended MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A, then wrapped to 64 characters) and for some reason openssl_pkey_get_public($pem) actually returned an OpenSSL Public Key now. I'm not really keen on using copy/paste solutions I don't understand, though, and the comment mentioned that this will only work for 2048-bit keys, which concerns me if we ever want to upgrade our security in the future.
After making the changes suggested to my key the new key looks like this:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wO7By66n38BCOOPqxnj
78gj8jMKACO5APe+M9wbwemOoipeC9DRCGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2
klyKF7dGQwecgf3So7bve8vvb+vD8C6loqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawy
m+OcE/0pzWlNV9asowIFWj/IXgDVKCFQnj164UFlxW1ITqLOQK1WlxqHAIoh20Rz
peJTlX9PYx3DDja1Pw7TPomHChMeRNswZ7zJiavYrBCTvYE+tm7JrPfbIfc1a9fC
Y3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCmQhPE4jcDINUds8bHu2on5NU5VmwHjQ46
xwIDAQAB
-----END PUBLIC KEY-----
(note that this is the same key, just with 32 magic bytes prepended to the beginning of it and "BEGIN RSA PUBLIC KEY" replaced with "BEGIN PUBLIC KEY")
2. The signature fails to verify (possibly because I'm using PS256 and not RS256)
Ignoring the issues with #1 for now and moving on to the next step, I tried to verify my signature like so:
$success = openssl_verify($jwtHeader . "." . $jwtBody, $jwtSignature, $key, OPENSSL_ALGO_SHA256);
This returned false. Meaning the signature was not valid. But I know the signature was valid because it worked fine in NodeJS. So I suspect the issue here revolves around my choice of algorithm.
How do I get PHP to properly verify this token?
Update 1
Here's the code that I'm using to verify my tokens in NodeJS. This is a HapiJS + TypeScript project, but you should be able to make sense of it. jwt is just defined as import * as jwt from "jsonwebtoken";:
jwt.verify(
token,
server.plugins["bf-jwtAuth"].publicKeys[tokenData.iss].key,
{
algorithms: [options.algorithm],
audience: userHash,
maxAge: options.tokenMaxAge
},
err =>
{
// We can disregard the "decoded" parameter
// because we already decoded it earlier
// We're just interested in the error
// (implying a bad signature)
if (err !== null)
{
request.log([], err);
return reject(Boom.unauthorized());
}
return resolve(h.authenticated({
credentials: {
user: {
id: tokenData.sub
}
}
}));
}
);
There's not too much to see here, because I just relied on a third-party tool to do all of the validation for me. jwt.verify(...) and it worked like magic.
Update 2
Assuming that my issue lie in the algorithm being used (PS256 vs RS256) I started searching around and found this StackOverflow post which pointed me at phpseclib
We actually coincidentally already had phpseclib installed via Composer as a dependency of Google's auth SDK, so I bumped it up to a top-level dependency and gave it a try. Unfortunately I still ran into an issue. Here's my code so far:
use phpseclib\Crypt\RSA;
// Setup:
$rsa = new RSA();
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
// The variables I'm working with:
$jwt = explode(".", "..."); // [Header, Body, Signature]
$key = "..."; // This is my PEM-encoded string, from above
// Attempting to verify:
$rsa->loadKey($key);
$valid = $rsa->verify($jwt[0] . "." . $jwt[1], base64_decode($jwt[2]));
if ($valid) { die("Valid"); } else { die("Invalid"); }
Neither die() statement is reached as I hit an error on the $rsa->verify() line with the following:
ErrorException: Invalid signature
at
/app/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php(2693)
Looking at this line in the library, it looks like it's failing at the "length checking" step:
if (strlen($s) != $this->k) {
user_error("Invalid signature");
}
I'm not sure what length it's expecting, though. I passed the raw signature directly from the JWT
After messing with this all day I finally figured out the missing piece.
First, some quick notes on the original question (already touched on in the updates):
To do RSA signatures with PSS padding ("PS256") you will need a third-party library, because the OpenSSL functions in PHP don't support this. A common recommendation is phpseclib
The 32 magic bytes I had to add to the key were only a quirk of PHP's OpenSSL functions and don't need to be utilized with phpseclib
With that said, my final problem (with the signature being "invalid") was:
JWT signatures are base64URL encoded, not base64 encoded
I wasn't even aware there was an RFC specification for base64URL encoding. It turns out you just replace every + with a - and every / with an _. So instead of:
$signature = base64_decode($jwt[2]);
It should be:
$signature = base64_decode(strtr($jwt[2], "-_", "+/"));
This works and my signature finally validates!

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

Hmac-sha1 in php

I've spent several hours reading through many posts already trying to get this to work and am still having issues with it. I'm trying to use the protectedpdf api which requires a three step process to login and receive an auth token, involving the hmac_sha1 hashing algorithm.
I'm making use of the hmac_hash() function in php so the code is simply:
$accessKey = "test";
$dataString = strtolower((string)$clientNonce.(string)$serverNonce.$accesskey);
$clientHash = strtoupper(hash_hmac('sha1', $dataString, $accessKey));
Where ClientNonce and ServerNonce are uuids like 30df805c-27d6-4df1-b482-48b685cc8f54
For a sample set of Client and Server Nonces, the built-in php function returns:
C39B753E50DB06DF3DE8E41C682FA8151B49ECBD and i get an authentication error in response to the login attempt.
Using the free formatter hmac generator online tool the same input yields:
7f48c3018c0e53a0b64a60bb7809c7a20a80c9a2
I would greatly appreciate any help - thanks!!!

Android In-App Billing Version 3 - Server-side Verification

I'm making Android Application with In-App Purchases. On Android Developer Center page I see that I must verify purchase data (json) with signature. I trying to use PHP tool from Google Code for this, but validation failed. First fail be that this library want from me not json (as I understand), but some plain text with fields, joined with : and |. It split this plain string to get packageName and validate it too. I commented this part of code, because next part more interesting:
$result = openssl_verify($responseData, base64_decode($signature),
$this->_publicKey, self::SIGNATURE_ALGORITHM);
//openssl_verify returns 1 for a valid signature
if (0 === $result) {
return false;
} else if (1 !== $result) {
require_once 'RuntimeException.php';
throw new AndroidMarket_Licensing_RuntimeException('Unknown error verifying the signature in openssl_verify');
}
where $responseData is my purchase json, self::SIGNATURE_ALGORITHM is OPENSSL_ALGO_SHA1, $this->_publicKey is:
$key = self::KEY_PREFIX . chunk_split($publicKey, 64, "\n") . self::KEY_SUFFIX;
$key = openssl_get_publickey($key);
if (false === $key) {
require_once 'InvalidArgumentException.php';
throw new AndroidMarket_Licensing_InvalidArgumentException('Please pass a Base64-encoded public key from the Market portal');
}
$this->_publicKey = $key;
where public key is base64 public key, like described:
Note:To find the public key portion of this key pair, open your application's
details in the Developer Console, then click on Services & APIs, and look at the
field titled Your License Key for This Application.
But such verification is fail. I read that API 3 is new (Dec 2012), and many other articles and tutorials isn't correspond to it. What I need to change to correct this verification?
This code using SHA1, but on Android Developer Center page (first link) described that public key is RSA with X.509... Any ideas?
UPD: While trying to make server always say 'purchase is ok' and add all purchases to database, find that this error is my fail. I take json to server in base64, since on server i base64_decode it in two different places, so I breaking it. This library works in part of code that use openssl to validate json. Previos version, as I understand, just validate package name; this may be easy rewrited to read productId from json.

Problems encoding Amazon Flexible Payments secret string in 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.

Categories