How dop I create a HMAC signature?

I am trying to talk to signed API endpoints on Binance.

I have managed to set headers and also send the timestamp. The final piece of the puzzle is to create a HMAC signiture.

My code as below where I send my actual API secret doesn't work.
I get the server time, but I also get the code :-1022, "msg":"Signiture for this request is not valid."}

I don't know how I go about creating a HMAC signiture from my API secret. Any advice? Would this be done via a JSON. If so, how would I go about doing this?

Thanks

_SECTION_BEGIN("Binance API Assets");


ih =InternetOpenURL("https://testnet.binance.vision/api/v3/time");
str = InternetReadString( ih );
timestamp = StrMid(str,14,13);
printf( "%s", timestamp);
InternetClose( ih );

InternetSetHeaders("X-MBX-APIKEY: WWNyvu#################AP1ku973");
ih = InternetOpenURL( "https://api.binance.com/sapi/v1/accountSnapshot?type=SPOT&limit=7&recvWindow=5000&timestamp="+timestamp+"&signature=qSzqUuN8#############Q2hPwSeMF");

printf( " Binance Assets " );
if( ih )
{
     while( ( str = InternetReadString( ih ) ) != "" )
     {
         printf( "%s", str );
     }
     InternetClose( ih );
}
_SECTION_END();

Ok, so an update.

I found this this code to create a HMAC. So I have have tried to run this code in Amibroker...

EnableScript("jscript");
<%
crypto = require('crypto');

query_string = 'timestamp=1578963600000';
apiSecret = 'NhqPtmdSJY#########################M6A7H5fATj0j';

function signature(query_string) {
    return crypto
        .createHmac('sha256', apiSecret)
        .update(query_string)
        .digest('hex');
}

console.log("hashing the string: ");
console.log(query_string);
console.log("and return:");
console.log(signature(query_string));

console.log("\n");

const another_query = 'symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559';
console.log(another_query);
console.log(signature(another_query));	
%>

to see how it works.
I get Error 90. Syntax error Microsoft JScript comliation error.

I have been reading this article to see how I would go about translating but I don't know if I am going about it correctly and don't really understand which statements I would append with AFL., since I am not taking anything out of my AFL (at the moment anyway.... I am sinmply tyring to make this part of the formula work,.... I will start passing variables in to the jscript later once I get the script working in the scripting host by itself.

Also, is it possible for jscript scripts run via the Amibroker engine to reference JS node modules, such as crypto (for HMAC signitures. I think crypto is a built in module within JS node now,... but I have installed it via 'npm install crypto' in any case.

As always, I appreciate your guidance.

The code you found is not for plain vanilla Javascript but for Node.js. It is different flavor of the language. The require keyword is NOT part of Javascript, see

and it is available in Node.js only. Node.js is standalone program (mainly for servers) that happens to use JavaScript for writing scripts. Node.js is a host for JavaScript. So AmiBroker is host for JavaScript. And you can’t have one host inside another host.

For SHA256 you would need to write the code yourself or use ready-to-use C code and use ADK to build a plugin.

1 Like

Thanks Tomas’s, that’s helpful.

I’m not certain I’m ready to take on the ADK….. maybe one day.
I have a developer friend who is looking at creating some executable OpenSSL HMAC functionwhich we’d write the output to a file, and use fgets() to read it.

I’ll post our efforts here if they are helpful.

Thanks again.

Ok, so a developer friend has managed to help create a HMAC signature using opesSSL and using a batch to write a 'signature.txt' from which we pull the signature. The procedure is as follows...

time =InternetOpenURL("https://testnet.binance.vision/api/v3/time");
if( time )
{
     while( ( str = InternetReadString( time ) ) != "" )
     {
		extracted = StrMid(str,14,13);
         printf( "%s", extracted);
     }
     InternetClose( time );
}


apikey="YOUR_API_KEY_HERE";
secret="YOUR_API_SECRET_HERE";

url = "https://api3.binance.com" + "/sapi/v1/accountSnapshot";

queryString = " recvWindow=5000&timestamp=" + extracted;
requestBody = "";

// use openssl to create signature.txt file
//opensslCmd = "echo -n \"" + queryString + requestBody + "\" |  \"C:\\program files\\OpenSSL-Win64\\bin\\openssl.exe \" dgst -sha256 -hmac " + secret+" > signature.txt";
//printf(opensslCmd);

executeRes = ShellExecute("test.bat" ,queryString,"" ,1 );
printf("ShellExecute result = %g", executeRes);

// read in the signature from signature.txt
signature = "";
fh = fopen( "signature.txt", "r");
if( fh )
{

   {
      signature = fgets( fh );
      printf("fgets = %s ",signature);   
   }
}
else
{
   printf("ERROR: file can not be found (does not exist)");
}
fclose(fh);

signature = StrReplace(signature, "SHA2-256(stdin)= ", "");
signature = StrReplace(signature, "\n", "");

printf(signature);

// set header
InternetSetHeaders("X-MBX-APIKEY: " + apikey);
// do http get request
ih = InternetOpenURL( url + queryString + "&signature=" + signature );
if( ih )
{
     while( ( str = InternetReadString( ih ) ) != "" )
     {
         printf( "%s", str );
     }
     InternetClose( ih );
}
else 
   printf("error getting from internet");

The test.bat file...

del signature.txt
echo -n %1 |  "C:\program files\OpenSSL-Win64\bin\openssl.exe " dgst -sha256 -hmac qSzqUuN8LVTxMuMZvRvEz08QOHJ2yfzUoSKN2l8r1Z0i3dGrpHmi092Q2hPwSeMF > signature.txt

So, we have the hashing of the key via openSSL, which is what this thread was about. However, we're not out of the woods, as Binance still isn't happy. The JSON (I think this is a JSON) from Binance reads...

1663205835743WWNyvuBbxTJYIe2P4YV80zx4iAX7KAFAfgWHIGwjC2LqPcszGg3LOUdXAP1ku973
qSzqUuN8LVTxMuMZvRvEz08QOHJ2yfzUoSKN2l8r1Z0i3dGrpHmi092Q2hPwSeMF
https://api3.binance.com/sapi/v1/accountSnapshot
 recvWindow=5000&timestamp=1663205835743

ShellExecute result = 42
fgets = SHA2-256(stdin)= 08bca132af726eaf6db06b111fe17b02efb248b0c1fc77bc9f6347f69c8acee0
 08bca132af726eaf6db06b111fe17b02efb248b0c1fc77bc9f6347f69c8acee0

08bca132af726eaf6db06b111fe17b02efb248b0c1fc77bc9f6347f69c8acee0
08bca132af726eaf6db06b111fe17b02efb248b0c1fc77bc9f6347f69c8acee0{"timestamp":1663205836583,"status":404,"error":"Not Found","message":"No message available","path":"/sapi/v1/accountSnapshot%20recvWindow=5000&timestamp=1663205835743&signature=08bca132af726eaf6db06b111fe17b02efb248b0c1fc77bc9f6347f69c8acee0"}

Debug session has ended.

I can see that after accounthosted there is a %20, so I have delted the space, and got pretty much the same output ".....v1/accountSnapshotrecvWindow=5000&timestamp=166320....." and I have also tried inserting & before recvWindow, but I still get the same JSON returned.

Any ideas on what I could be doing wrong? Wallet endpoints are here and signing instructions are here

@Tomasz I'm wondering if I should post this as a separate topic since the OP was about HMAC signitures, which I am 95% sure we have cracked so I'm wondering if the thread is actually solved, or, not as it's part of a larger project.

For GET query string there is no space and %20, ? is used to delimit URI and the query parameters follow.

Error response says it got the path wrong

"path":"/sapi/v1/accountSnapshot%20recvWindow=5000&times
1 Like

Thanks for that. That's helpful, I think I'm getting closer. I've actually realised that it's a different endpoint for a sub account, e.g

"/sapi/v3/sub-account/assets";

and I have replaced that. I've also dropped the %20 as suggested (I was previously wondering if we needed to escape a space) and I'm getting the same JSON response as before.

time =InternetOpenURL("https://testnet.binance.vision/api/v3/time");
if( time )
{
     while( ( str = InternetReadString( time ) ) != "" )
     {
		extracted = StrMid(str,14,13);
         printf( "%s", extracted);
     }
     InternetClose( time );
}


apikey="YOUR_API_KEY_HERE";
secret="YOUR_API_SECRET_HERE";

url = "https://api1.binance.com" + "?/sapi/v3/sub-account/assets";

queryString = "?recvWindow=5000&timestamp=" + extracted;
requestBody = "";

// use openssl to create signature.txt file
//opensslCmd = "echo -n \"" + queryString + requestBody + "\" |  \"C:\\program files\\OpenSSL-Win64\\bin\\openssl.exe \" dgst -sha256 -hmac " + secret+" > signature.txt";
//printf(opensslCmd);

executeRes = ShellExecute("test.bat" ,queryString,"" ,1 );
printf("ShellExecute result = %g", executeRes);

// read in the signature from signature.txt
signature = "";
fh = fopen( "signature.txt", "r");
if( fh )
{

   {
      signature = fgets( fh );
      printf("fgets = %s ",signature);   
   }
}
else
{
   printf("ERROR: file can not be found (does not exist)");
}
fclose(fh);

signature = StrReplace(signature, "SHA2-256(stdin)= ", "");
signature = StrReplace(signature, "\n", "");

printf(signature);

// set header
InternetSetHeaders("X-MBX-APIKEY: " + apikey);

// do http get request
//querystring = StrReplace(querystring, "^","");

ih = InternetOpenURL( url + queryString + "&signature=" + signature );
if( ih )
{
     while( ( str = InternetReadString( ih ) ) != "" )
     {
         printf( "%s", str );
     }
     InternetClose( ih );
}
else 
   printf("error getting from internet");

gives

.. "timestamp":1663413240822,"status":404,"error":"Not Found","message":"No message available","path":"/sapi/v3/sub-account/assetsrecvWindow=5000&timestamp=1663413239992&signature=e562e7083ace52bee714936bc043fb42099f86c12ddb59f27e90cee2fafa69ef"}

I note that you have said that we delimit the URL and query parameters with ?, which I think looks like this...


apikey="YOUR_API_KEY_HERE";
secret="YOUR_API_SECRET_HERE";

url = "https://api1.binance.com" + "?/sapi/v3/sub-account/assets";

queryString = "?recvWindow=5000&timestamp=" + extracted;
requestBody = "";

That gives me

1663413###################################TKtgTvU
45xP3#########################################Z1H1q
https://api1.binance.com?/sapi/v3/sub-account/assets
?recvWindow=5000&timestamp=1663413499092

ShellExecute result = 42
fgets = SHA2-256(stdin)= 6bc072##################################################33749dc
 6bc072##################################################f33749dc

6bc072##################################################f33749dc
6bc072##################################################33749dc<!DOCTYPE html><html><head><title>Test OK</title></head><body></body></html>

Debug session has ended.

I have only just re-created the API key and secret and set the permissions, so it should be working.

Can you see what I am messing up? :face_with_spiral_eyes:

It is very simple if you read their documentation clearly and in general the https url.
you seem to be hurrying too much and overlooking things. Im saying same thing since the JSON thing you did.
You cant have two ?. The first one is not required. it is continuous api path with out any space or new line like this

https://api1.binance.com/sapi/v3/sub-account/assets?recvWindow=5000&timestamp=1663413499092
1 Like

Grrrrr. I still can't make it work. I'm not a coder/programmer by trade.

I followed your instruction and I've read the examples offered by Binance and their documentation. It is encouraging that you find it simple, as at least that means it can be done. But the documentation offered by Binance doesn't make it clear to me. I don't understand exactly what it is we would send to Binance, what they want hashed, and what they want sent as a string.

E.g would we hash

assets?recvWindow=5000&timestamp=1663413499092

(lets say the result from hashing the above string is '12345')

....as one hash then after, hash the API secret ...

abcdefghijklmonpqrstuvwxyz1234567890

(lets say the result from hashing this is 678910)

and then send the URL as

https://api1.binance.com/sapi/v3/sub-account/12345&signature=678910)

or would I rather send

https://api1.binance.com/sapi/v3/sub-account/assets?recvWindow=5000&timestamp=12345&signature=678910)

Again, thanks for your patience.

 url =  BASE_URL + url_path + "?" + query_string + "&signature=" + hashing(query_string)

This is how it should look like.

BASE_URL = "https://api1.binance.com"; 
url_path = "/sapi/v3/sub-account/assets";

query_string = <Everything after ? that is data to be sent including timestamp> so
query_string = recvWindow=5000&timestamp=1663413499092

signature= hashing_function(query_string )  so
signature=e562e7083ace52bee714936bc043fb42099f86c

Dont miss the & in query_string&signature=

so putting it together is

https://api1.binance.com/sapi/v3/sub-account/assets?recvWindow=5000&timestamp=12345&signature=e562e7083ace52bee714936bc043fb42099f86c

Eg. The query_string could be as long as this one too

query_string="symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559"
3 Likes

Awesome. Thanks for that. It makes sense. I’ll check that out after work this evening. :grinning:

Awesom. Thanks for that. It makes sense. I cannot see though where I place my secret key. Should I add it to the query_string before hashing, e.g


recvWindow=5000&timestamp=1663413499092&secretKey=<API_SECRET_HERE>

then I hash all of that and add it at the end, eg


recvWindow=5000&timestamp=1663413499092&secretKey=<API_SECRET_HERE>&signature=<output_of_hashing_algorythm>

or is my secret key going somewhere else?

Noooo...
Secret is not shared anywhere. Then it defeats the purpose

signature= hashing_function(query_string )  so
signature=e562e7083ace52bee714936bc043fb42099f86c
//  // this line i mentioned above

The API_SECRET is passed to the hashing_function( query_string, API_Secret)

// return hash of ( query_string and api_secret)  using sha256
// what is returned is put in the signature=< hasing fun string returned here>

You are not supposed to create one signature and sleep on it. They want every API request from your end to have a plaintext querystring & signature that is hash( querystring, api_secret )

3 Likes

Ahhhh, right. Perfect. I’ll sort that out. Thanks so much!

I started again and have taken on board what you have kindly explained. My understanding is that we cannot hash directly from AFL so I have used a batch file to mnake use of openSSL and have taken the examples from the binance website.
Also, as I have no assets in that sub account, and in case the reason I was not having anything returned was because there were no assets, I changes the endpoint to list. My code is thus;

/*Define timestamp, apikey, api secret, base URL of API, path of API and query string.
Note that querystring should always include at least "recvWindow=5000&timestamp=" as well 
as the timestamp itself, which is taken from binance*/
time =InternetOpenURL("https://testnet.binance.vision/api/v3/time");
if( time )
{
     while( ( str = InternetReadString( time ) ) != "" )
     {
		extracted = StrMid(str,14,13);
         printf( "%s", extracted);
     }
     InternetClose( time );
}
apikey="<API_KEY_HERE>";
secret="<API_SECRET_HERE>";
BASE_URL = "https://api1.binance.com";
url_path = "/sapi/v3/sub-account/list";
query_string = "recvWindow=5000&timestamp=" + extracted;



/*
Delete the old querystring batch file and signature file. Write new batch file, "querystring.bat", then execute that batch file to write a file "signature.txt" containing signature.
Read from that file to define the signature
*/
fdelete("querystring.bat");
fdelete("signature.txt");
ThreadSleep(100);
string_inside_batch ="echo -n " + "\"" + query_string + "\" " + "| openssl dgst -sha256 -hmac " + secret + " > \"signature.txt\"";
fh = fopen("C:\\Program Files\\AmiBroker\\querystring.bat", "w");
if (fh)
{
	fputs(string_inside_batch,fh);
}
fclose(fh);
ShellExecute("querystring.bat" ,"","",1);
ThreadSleep(100);
signature ="";
fh = fopen("signature.txt", "r");
if (fh)
{
		signature = fgets(fh);
}
fclose(fh);
signature = StrReplace(signature,"SHA2-256(stdin)= ","");
url = BASE_URL + url_path + "?" + query_string + "&signature=" + signature;



/*
set header and open URL
*/
InternetSetHeaders("X-MBX-APIKEY: " + apikey);
ih = InternetOpenURL(url);
if( ih )
{
     while( ( str = InternetReadString( ih ) ) != "" )
     {
         printf( "%s", str );
     }
     InternetClose( ih );
}
else 
   printf("error getting from internet");

Running this, in the amibroker output I get;

1663643579456<API_KEY_HERE>
<API_SECRET_HERE>
https://api1.binance.com
/sapi/v3/sub-account/list
recvWindow=5000&timestamp=1663643579456
echo -n "recvWindow=5000&timestamp=1663643579456" | openssl dgst -sha256 -hmac <API_SECRET_HERE> > "signature.txt"

029c64b09faeb7a4415d3d204d07fc0103c2c98bf50e7c991569a35346880b31

https://api1.binance.com/sapi/v3/sub-account/list?recvWindow=5000&timestamp=1663643579456&signature=029c64b09faeb7a4415d3d204d07fc0103c2c98bf50e7c991569a35346880b31

{"code":-1022,"msg":"Signature for this request is not valid."}

Debug session has ended.

At least now, I get a different signature every time in the signature.txt file, but unfortunately, it is not being accepted by Binance. :frowning:

Thats because your signature is invalid :smiley:

Now this is what you need to test, because the quotes and -n seem to be considered as part of the string
I get different output with and without "" quotes in CMD prompt

image

but not the case in powershell so check the behaviour of your echo command
image

In cmd prompt, anything after echo including white space is part of the string

echo -n "text"
-n "text"  // I get this output

Test the command properly in powershell to get a matching output or use powershell.

see if this helps

3 Likes

This works

secret = query_string = "test";
string_inside_batch ="echo "+ query_string + "| openssl dgst -sha256 -hmac " + secret + " > D:\\signature.txt";
fh = fopen("D:\\querystring.bat", "w");
fputs(string_inside_batch,fh);
fclose(fh);

There is no space even after querystring in CMD

CMD>echo string| openssl dgst -sha256 -hmac apikey

PS> echo "astring" | openssl dgst -sha256 -hmac mykey  // Here space and quotes are ok
3 Likes

Brilliant!! :smiley:
Thanks so much for that and all of the support here. I have no idea why, but no matter how I tried, I could not get the same output from the HMAC function as is documented here when using either powershell or cmd to call the openssl hmac function.

I have downloaded gitbash, set gitbash tobe the default console for .sh files and have used that instead, and now, finally, I am successfully signing endpoints.
The code is as follows;

/*
Define the time, apikey, apisecret, the BASE_URL, the url path (endpoint) and querystring.
*/
time =InternetOpenURL("https://testnet.binance.vision/api/v3/time");
if( time )
{
     while( ( str = InternetReadString( time ) ) != "" )
     {
		extracted = StrMid(str,14,13);
         printf( "%s", extracted);
     }
     InternetClose( time );
}
apikey="<YOUR_API_KEY_HERE>";
secret="<YOUR_API_SECRET_HERE>";
BASE_URL = "https://api1.binance.com";
url_path = "/sapi/v1/account/apiTradingStatus";
query_string = "recvWindow=5000&timestamp=" + extracted;


/*
Remove old signature. Define a string of a file that you will use a shell to execute, open the file then place that string 
inside the file. Close the file
*/
fdelete("signature.txt");

string_inside_batch = 
"#!/usr/bin/env bash"
+"\n"
+"\nSECRET=\"" + secret + "\""
+"\n"
+"\nQUERY_STRING=\"" + query_string + "\""
+"\n"
+ "\necho " + "\"hashing string\""
+ "\necho $QUERY_STRING"
+ "\necho " + "\"and return\""
+ "\necho -n $QUERY_STRING |" + " \\"
+ "\nopenssl dgst -sha256 -hmac $SECRET >> signature.txt";

fh = fopen("C:\\Program Files\\AmiBroker\\binancesig.sh", "w");
if (fh)
{
	fputs(string_inside_batch,fh);
}
fclose(fh);

/*
Execute the file using gitbash. This will write a file with the signiture on. Pause before referencing the file so that 
there is a file to reference later in the script
*/

ShellExecute("binancesig.sh" ,"","",1);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);
ThreadSleep(100);



/*
Initialise the signature variable. Open the signature.txt to read the signature. Use strReplace to remove '(stdin)= '
therefore making the signature useable. Create a URL of the BASE_URL, url path, "?"(for query), query_string, &signature and append with the signature
you have extracted from signature.txt 
Set header with ApI key (not secret), and open the URL
*/
signature = "";
fh = fopen("C:\\Program Files\\AmiBroker\\signature.txt", "r");
if( fh )
{
   while( ! feof( fh ) )
   {
      signature = fgets( fh ); 
      signature = StrReplace(signature,"(stdin)= ","");
      url = BASE_URL + url_path + "?" + query_string + "&signature=" + signature;
      
      InternetSetHeaders("X-MBX-APIKEY: " + apikey);
      io = InternetOpenURL(url);
		if( io )
			{
					while( ( str = InternetReadString( io ) ) != "" )
						{
					printf( "%s", str );
						}
				InternetClose( io );
			}
else 
   printf("error getting from internet");
	}
}
else
{
   printf("ERROR: file can not be found (does not exist)");
}
fclose(fh);

This does work and I have been able to reference my own account. I now just need to go and learn how the API works, and limits and weights and all of that stuff I don't understand.

Thanks once again!!
:smiley:

2 Likes
cd /D "C:\PATH\OF\BATCH\FILE"

openssl dgst -sha256 -hmac NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j -out file_out.txt  
 file_in.txt

// Store above command in a .bat file
# hashing string
timestamp=1578963600000
# and return
d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4

This works. somehow the input data is being encoded differently by CMD/PS. Anyway, openssl can read querystring from input file(file_in.txt) and write output to file_out.txt
( only a couple of spaces between output_file and input_file. its not newline and no flags/options)

The input and output matches those on github.

Dont need to delete the files each time. Just use 'w' write option in AFL and openssl, both will overwrite the old data.
So write querystring to input file and read the signature in output file. No need for path to both files. Just fix the current working DIR in cd command in the batch.

There could be better ways but given your level, you can create just one batch file and dont need to write the script each time. Keep the API_KEY in it. Now all you are doing is just executing/calling .bat file each time.
The QUERY_STRING is written to input file
read output_file for signature.

1 Like

Even this works directly without the need of batch file / script file.

ShellExecute("openssl", " dgst -sha256 -hmac NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j -out file_out.txt  file_in.txt", "C:\\PATH\\TO\\both_files", 1);
3 Likes