I have been working on a way to implement HMAC verification in python with flask for the selly.gg merchant website.
So selly's dev documentation give these following examples to verify HMAC signatures (in PHP and ruby): https://developer.selly.gg/?php#signing-validating
(code below:)
PHP:
<?php
$signature = hash_hmac('sha512', json_encode($_POST), $secret);
if hash_equals($signature, $signatureFromHeader) {
// Webhook is valid
}
?>
RUBY:
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), secret, payload.to_json)
is_valid_signature = ActiveSupport::SecurityUtils.secure_compare(request.headers['X-Selly-Signature'], signature)
So, so far what I could figure out: They don't encode with base64 (like shopify and others do), it uses SHA-512, it encodes the secret code alongside json response data and finally the request header is 'X-Selly-Signature'
I've made the following code so far (based on shopify's code for HMAC signing https://help.shopify.com/en/api/getting-started/webhooks):
SECRET = "secretkeyhere"
def verify_webhook(data, hmac_header):
digest = hmac.new(bytes(SECRET, 'ascii'), bytes(json.dumps(data), 'utf8'), hashlib.sha512).hexdigest()
return hmac.compare_digest(digest, hmac_header)
try:
responsebody = request.json #line:22
status = responsebody['status']#line:25
except Exception as e:
print(e)
return not_found()
print("X Selly sign: " + request.headers.get('X-Selly-Signature'))
verified = verify_webhook(responsebody, request.headers.get('X-Selly-Signature'))
print(verified)
However selly has a webhook simulator, and even with the proper secret key and valid requests, the verify_webhook will always return False. I tried contacting Selly support, but they couldn't help me more than that
You can test the webhook simulator at the following address:
https://selly.io/dashboard/{your account}/developer/webhook/simulate
You're nearly right except that you don't need to json.dumps the request data. This will likely introduce changes into output, such as changes to formatting, that won't match the original data meaning the HMAC will fail.
E.g.
{"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"}
is different to:
{
"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"
}
which is actually:
{x0ax20x20"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"x0a}
A hash will be completely different for the two inputs.
See how json.loads and json.dumps will modify the formatting and therefore the hash:
http_data = b'''{
"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"
}
'''
print(http_data)
h = hashlib.sha512(http_data).hexdigest()
print(h)
py_dict = json.loads(http_data) # deserialise to Python dict
py_str = json.dumps(py_dict) # serialise to a Python str
py_bytes = json.dumps(py_dict).encode('utf-8') # encode to UTF-8 bytes
print(py_str)
h2 = hashlib.sha512(py_bytes).hexdigest()
print(h2)
Output:
b'{\n "id":"fd87d909-fbfc-466c-964a-5478d5bc066a"\n}\n'
364325098....
{"id": "fd87d909-fbfc-466c-964a-5478d5bc066a"}
9664f687a....
It doesn't help that Selly's PHP example shows something similar. In fact, the Selly PHP example is useless as the data won't be form encoded anyway, so the data won't be in $_POST!
Here's my little Flask example:
import hmac
import hashlib
from flask import Flask, request, Response
app = Flask(__name__)
php_hash = "01e5335ed340ef3f211903f6c8b0e4ae34c585664da51066137a2a8aa02c2b90ca13da28622aa3948b9734eff65b13a099dd69f49203bc2d7ae60ebee9f5d858"
secret = "1234ABC".encode("ascii") # returns a byte object
#app.route("/", methods=['POST', 'GET'])
def selly():
request_data = request.data # returns a byte object
hm = hmac.new(secret, request_data, hashlib.sha512)
sig = hm.hexdigest()
resp = f"""req: {request_data}
sig: {sig}
match: {sig==php_hash}"""
return Response(resp, mimetype='text/plain')
app.run(debug=True)
Note the use of request.data to get the raw byte input and the simple use of encode on the secret str to get the encoded bytes (instead of using the verbose bytes() instantiation).
This can be tested with:
curl -X "POST" "http://localhost:5000/" \
-H 'Content-Type: text/plain; charset=utf-8' \
-d "{\"id\":\"fd87d909-fbfc-466c-964a-5478d5bc066a\"}"
I also created a bit of PHP to validate both languages create the same result:
<?php
header('Content-Type: text/plain');
$post = file_get_contents('php://input');
print $post;
$signature = hash_hmac('sha512', $post, "1234ABC");
print $signature;
?>
Related
I'm using php 8.0.11, i have to generate a SHA256 encrypted messagesignature.When i test the API in postman with javascipt code in Pre-request script it give the right encrypted messagesignature, i converted the script to php when i test it in php it sends a different wrong encrypted messagesignature (key & msg are fake) :
javascript code (Pre-request script in postman):
let msg='mymessage'
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,"myapipkey");
hmac.update(msg);
const messageSignature = hmac.finalize().toString();
pm.globals.set("messageSignature",messageSignature);
console.log('messageSi:',pm.globals.get('messageSignature'))
````
php code:
````php
$data_to_hash = "mymessage";
$data_hmac=hash('sha256', $data_to_hash);
$ctx = hash_init('sha256', HASH_HMAC, 'myapipkey');
hash_update($ctx, $data_hmac);
$result = hash_final($ctx);
echo $result;
````
A simple change to the PHP code should give the correct result.
It looks like you were hashing twice (or something like that!)
$data_to_hash = "mymessage";
$ctx = hash_init('sha256', HASH_HMAC, 'myapipkey');
hash_update($ctx, $data_to_hash);
$result = hash_final($ctx);
echo $result;
In any case, the output of the above code will be:
898786a1fa80da9b463c1c7c9045377451c40cf3684cbba73bdfee48cd3a5b8f
Which is the same as the JavaScript code, both match the output given here:
https://codebeautify.org/hmac-generator
With Algorithm = 'SHA256', Key = 'myapipkey' and Plaintext = 'mymessage'.
I am trying to use python to login to my bittrex account and set up an automatic trading bot (written by myself in python).
However my problem is that I cannot authenticate myself. There is an API document provided by bittrex, but the code is in PHP (and i really dont know PHP)...
The main problem is that I am was not involved so far in the hmac hash authentication... so I am kinda lost here.
The PHP code provided by bittrex is the following:
$apikey='xxx';
$apisecret='xxx';
$nonce=time();
$uri='https://bittrex.com/api/v1.1/market/getopenorders?apikey='.$apikey.'&nonce='.$nonce;
$sign=hash_hmac('sha512',$uri,$apisecret);
$ch = curl_init($uri);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('apisign:'.$sign));
$execResult = curl_exec($ch);
$obj = json_decode($execResult);
I also tried to make up something based on other stackoverflow posts... but all I could come up was this:
import hashlib
import hmac
import requests
import time
apikey = 'xxx'
apisecret = 'xxx'
def request_comkort(url, payload ):
tosign = "&".join( [i + '=' + payload[i] for i in payload] )
sign = hmac.new( apisecret, tosign , hashlib.sha512);
headers = {'sign': str(sign.hexdigest()), 'nonce': str(int(time.time())), 'apikey': apikey }
r = requests.post(url, data=payload, headers=headers)
print tosign, '\n'
print sign, '\n'
print headers, '\n'
print url, '\n'
print payload, '\n'
print headers, '\n'
return r.text
print request_comkort("https://bittrex.com/api/v1.1/account/getbalances", {})
Unfortunately it doesn't really work..
I get error:
{"success":false,"message":"APIKEY_NOT_PROVIDED","result":null}
If someone could help me out or point me in the right direction, that would be awesome. I can hardly wait to run my trading bot...:)
Thank you in advance!
I was having the same problem and came up with this.
Unfortunally I have passes the key and nonce problem but having issues with secret key. Not very familiar with encryption. If you can help...
Here is the code. Notice I{m using python 2.7 on ubuntu. so requests and urllib may work diferent to your version
enter code here
import json
import time
import hashlib
import hmac
nonce = str('{:10.0f}'.format(time.time()))
apikey = 'mykey'
apisecret = 'mysecret'
url = 'https://api.bittrex.com/api/v1.1/account/getbalances?apikey=apikey'
url += '&nonce=' + str(int(time.time()))
sign = hmac.new(b'apisecret', b'url', hashlib.sha512).hexdigest()
headers = {'&secret': sign}
request = requests.get(url, sign)
balance = json.loads(urllib.urlopen(url).read())
print(balance)
How to receive raw JSON response from HTTP POST webhook?
I am working with an API and to verify that the POST to the webhook is indeed from the appropriate company API, they suggest this method:
To allow a client to verify a webhook message has in fact come from SIGNIFYD, an X-SIGNIFYD-SEC-HMAC-SHA256 header is included in each webhook POST message. The contents of this header is the Base64 encoded output of the HMAC SHA256 encoding of the JSON body of the message, using the team's API key as the encryption key. To verify the authenticity of the webhook message, you should calculate this value yourself and verify it equals the value contained in the header.
For the test environment, the "secret" key is ABCDE instead of the "Team API key."
I am receiving it in PHP like so:
<?php
// Get relevant Signifyd custom headers to be used for verification
$header_sig_topic = $_SERVER['HTTP_X_SIGNIFYD_TOPIC'];
$header_sig_sec_hmac = $_SERVER['HTTP_X_SIGNIFYD_SEC_HMAC_SHA256'];
// Get POST body
$webhookContent = "";
$webhook = fopen('php://input' , 'r');
while (!feof($webhook)) {
$webhookContent .= fread($webhook, 4096);
}
fclose($webhook);
?>
then I am processing it into the hash like so:
<?php
$sig_ver_sha = hash_hmac('sha256', $webhookContent, $secret);
$sig_ver_hash = base64_encode( $sig_ver_sha );
?>
However, I am going wrong somewhere, because the hash I calculate is
OTc1YzExZDY2ZTE1MTVmYmJmNWNhNDRhNWMxZGIzZDk0NmM3OGE4NDU2N2JkYTJmZDJlYWI0ODRhNjlhNTdiYg==
while the header for an identical sample response header always comes with
W+D70ded8u5DG7P4BcG0u2etvAqQZvxz70Q4OXh0vlY=
I thought I was getting the JSOn body wrong somehow so I've tried every combination of json_encode and json_decode but nothing helps, my hash never matches.
I've also tried using $webhookContent = json_decode(file_get_contents('php://input'), true); to store the POST body but that just comes up empty ($_POST doesn't work either).
Am I doing something else wrong other than receiving the JSON?
The JSON that comes as the body of the test response which always comes with W+D70ded8u5DG7P4BcG0u2etvAqQZvxz70Q4OXh0vlY= as the hash key to be used for verification:
{ "analysisUrl": "https://signifyd.com/v2/cases/1/analysis",
"entriesUrl": "https://signifyd.com/v2/cases/1/entries", "notesUrl":
"https://signifyd.com/v2/cases/1/notes", "orderUrl":
"https://signifyd.com/v2/cases/1/order", "guaranteeEligible":false,
"status":"DISMISSED", "uuid":"709b9107-eda0-4cdd-bdac-a82f51a8a3f3",
"headline":"John Smith", "reviewDisposition":null, "associatedTeam":{
"teamName":"anyTeam", "teamId":26, "getAutoDismiss":true,
"getTeamDismissalDays":2 }, "orderId":"19418",
"orderDate":"2013-06-17T06:20:47-0700", "orderAmount":365.99,
"createdAt":"2013-11-05T14:23:26-0800",
"updatedAt":"2013-11-05T14:23:26-0800",
"adjustedScore":262.6666666666667, "investigationId":1,
"score":262.6666666666667, "caseId":1,
"guaranteeDisposition":"APPROVED"}
If it helps to see where I'm going wrong, an example is provided but it's in Python:
Mac sha256HMAC = javax.crypto.Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(teamAPIKEY.getBytes(), "HmacSHA256");
sha256HMAC.init(secretKey);
String encodedHMAC256 = Base64.encodeBase64String(sha256HMAC.doFinal(jsonBody.getBytes("UTF-8")));
My error was in simply not specifying the $raw_output parameter of the hash_hmac() function as true.
raw_output
When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits.
So, since I wasn't specifying $raw_output as true, I was getting hexits instead of raw binary output, which looked like this: 975c11d66e1515fbbf5ca44a5c1db3d946c78a84567bda2fd2eab484a69a57bb.
EDIT: The answer here is
<?php
$sig_ver_sha = hash_hmac('sha256', $webhookContent, $secret, true);
$sig_ver_hash = base64_encode( $sig_ver_sha );
?>
I'm using a simple PHP script to verify Android order to parse download for the customer.
$receipt = $_GET['purchaseData'];
$billInfo = json_decode($receipt,true);
$signature = $_GET['dataSignature'];
$public_key_base64 = "xxxxxxxxxxxxxxxx";
$key = "-----BEGIN PUBLIC KEY-----\n".
chunk_split($public_key_base64, 64,"\n").
'-----END PUBLIC KEY-----';
$key = openssl_get_publickey($key);
$signature = base64_decode($signature);
//$result = openssl_verify($billInfo, $signature, $key);
$result = openssl_verify($receipt, $signature, $key);
if (0 === $result) {
echo "0";
} else if (1 !== $result) {
echo "1";
} else {
echo "Hello World!";
}
//added the var_dump($result); as asked by A-2-A
var_dump($result);
result is 0int(0)
I made a real order through the App after I published it and when trying to validate the order I get "0" as result.
I tried direct HTTP access
https://domain.com/thankyou.php?purchaseData={"packageName":"com.example.app","orderId":"GPA.1234-5678-1234-98608","productId":"product","developerPayload":"mypurchasetoken","purchaseTime":1455346586453,"purchaseState":0,"developerPayload":"mypurchasetoken","purchaseToken":"ggedobflmccnemedgplmodhp...."}&dataSignature=gwmBf...
I'm keeping the first of the question because my result is still a guess. After further investigation I think it's the signature not being read in a nice clean way as sent by google.
The signature=gwmBfgGudpG5iPp3L0OnepNlx while the browser is reading it as ƒ ~®v‘¹ˆúw
How is it possible to let it be read in the right way?
To verify the signature you want to make sure of the following:
INAPP_PURCHASE_DATA is not mutated in any way. Any encoding or escaping changes will result in a invalid verification. The best way to ensure it gets to your server intact is to base64 encoded it.
INAPP_DATA_SIGNATURE also must remain intact, it should already base64 encoded so sending that to your server should not be a problem.
openssl_verify expects both data and signature arguments to be in their raw state, so base64 decode before verifying.
It also takes signature_alg as the last argument, in this case sha1WithRSAEncryption should work as should the default, but if in doubt try a few other sha1 algorithms to see which ones work.
My best guess why it's not working for you right now is that you're not receiving the INAPP_PURCHASE_DATA on your server in the same condition that it was received on the app. This Stackoverflow question had the same problem.
I built a PHP file with the sole purpose of hiding the API keys for Google Search, but part of the file_get_contents() always echo angular.callbacks._0_({ instead of angular.callbacks._0({
This small change makes the rest of the response worthless as Angular throws Uncaught TypeError: angular.callbacks._0_ is not a function. Although the workaround does works flawlessly, I would like to know if someone found the root of this issue or a better solution that is strictly PHP (no curl or any other package.)
search.php
<?php // Created by Deovandski on 2/14/2016
header('Content-type: application/json');
# Setup Base URL and array for Parameters
$host = 'https://www.googleapis.com/customsearch/v1?';
$queries = array();
$queries['cx'] = "XXX";// CSE KEY
$queries['key'] = "XXX"; // API KEY
# Setup possible incoming params
if (isset($_GET['search_term'])) $queries['q'] = $_GET['search_term'];
if (isset($_GET['result_count'])) $queries['result_count'] = $_GET['result_count'];
if (isset($_GET['callback'])) $queries['callback'] = $_GET['callback'];
# Build query and Final URL
$queriesURL = http_build_query($queries) . "\n";
$finalURL = $host.$queriesURL;
echo $finalURL;
/* echo $finalURL output (I only edited the keys out):
https://www.googleapis.com/customsearch/v1?cx=XXX&key=XXX&q=Hatsune+Miku&result_count=10&callback=angular.callbacks._0
*/
// Setup Response
$response = file_get_contents($finalURL);
// workaround
$fixedResponse = str_replace("angular.callbacks._0_", "angular.callbacks._0", $response);
echo $fixedResponse;
?>
This is part of a correct Google API response:
// API callback
angular.callbacks._0({
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&cref={cref?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&relatedSite={relatedSite?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
},
I put up a live version of this issue that can be seen on my FTP server. The PHP file can be viewed through this link (AngularJS parameters included on it).
The problem is the escape sequences \n. Which is passed as part of the request. And which is interpreted as space and as part of the callback function name and replaced by the side of the API to underline.
To understand just try this option and look at the result:
$queriesURL = http_build_query($queries) . "\n" . "after";
So just take away a newline.