{“error”:“Invalid API key\/secret pair.”} API POST request with C++ and libcurl

I don't think I'm conforming to the Poloniex API rules.
I don't know if it's just me, but they are seeming to make it more complicated than what it really is.
The API rules are commented in the code.

I get the output {"error":"Invalid API key\/secret pair."}
So I'm assuming it's the way I'm calculating the HMAC SHA512 Algorithm.
Located in
Poloniex.cpp on line 28.

I tried conforming to these rules mentioned from a different site:

You are doing a GET-request. You have to do a POST-request for private functions. From the documentation:

All calls to the trading API are sent via HTTP POST to https://poloniex.com/tradingApi and must contain the following headers:

Key - Your API key.
Sign - The query's POST data signed by your key's "secret" according to the HMAC-SHA512 method.
Additionally, all queries must include a "nonce" POST parameter. The nonce parameter is an integer which must always be greater than the previous nonce used.

That means Key and Sign are sent inside the HTTP-Headers. The rest (command, nonce) are part of the body.

Example request:

1. Let's say your API-Key is 123 and your API-Secret is 456.
2. The parameters for a request to returnBalances are command=returnBalances&nonce=1473087174. Please note: The nonce-parameter must be increased with every request. It is recommended to use the current timestamp.
3. You sign command=returnBalances&nonce=1473087174 using HMAC-SHA512 and your secret (456). The result will be put into the Sign-Header.
4. You put your API-Key (123) into the Key-Header.
5. You put the request parameters command=returnBalances&nonce=1473087174 into the request-body.
6. You send your request to https://poloniex.com/tradingApi using the POST-method and using SSL-encryption.

Doing this over your browser will not work unless you use third-party-software/plugins which allow you to modify the request-headers, etc.


Poloniex.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma once

// All calls to the trading API are sent via HTTP POST to https://poloniex.com/tradingApi and must contain the following headers:
// Key - Your API key.
// Sign - The query's POST data signed by your key's "secret" according to the HMAC-SHA512 method.
// Additionally, all queries must include a "nonce" POST parameter. The nonce parameter is an integer which must always be greater than the previous nonce used.

#include <ctime>
#include <curl/curl.h>
#include <iostream>
#include <openssl/hmac.h>
#include <string>
#include <vector>
 
class Poloniex
{
private:
	std::string api_key;
	std::string api_secret;
	std::string public_url{"https://poloniex.com/public"};
	std::string trading_url{"https://poloniex.com/tradingApi"};

public:
	Poloniex(std::string, std::string);
	
	// Sample query to get all currency balances.
	void queryBalances();
};


Poloniex.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "Poloniex.h"

// All calls to the trading API are sent via HTTP POST to https://poloniex.com/tradingApi and must contain the following headers:
// Key - Your API key.
// Sign - The query's POST data signed by your key's "secret" according to the HMAC-SHA512 method.
// Additionally, all queries must include a "nonce" POST parameter. The nonce parameter is an integer which must always be greater than the previous nonce used.

Poloniex::Poloniex(std::string key, std::string secret) : api_key{ key }, api_secret{ secret } {}

// Sample query to get all currency balances.
void Poloniex::queryBalances()
{
	// Generate command.
	std::string command{ "command=returnBalances" };

	// Generate nonce.
	std::time_t n = std::time(0);
	std::string nonce = "&nonce=" + std::to_string(n);

	// Generate POST data string.
	std::string post_data{ command + nonce };

	// HMAC.
	std::string key = "Key: " + api_key;
	unsigned char* heliosRageBuffer = HMAC(EVP_sha512(), api_secret.c_str(), strlen(api_secret.c_str()),(unsigned char*)post_data.c_str(), strlen(post_data.c_str()), NULL, NULL);

        // Converting to hex string.
        std::stringstream ss;
	for (int i = 0; i != 64; ++i)
	{
		if (static_cast<int>(heliosRageBuffer[i]) < 0x10)
			ss << std::hex << std::setw(1) << static_cast<int>(heliosRageBuffer[i]);
		else
			ss << std::hex << static_cast<int>(heliosRageBuffer[i]);
	}
	std::string sign = "Sign: " + ss.str();

	// Time to Curl.
	CURL *curl;
	CURLcode res;

	// In windows, this will init the winsock stuff.
	curl_global_init(CURL_GLOBAL_ALL);

	// Get a curl handle.
	curl = curl_easy_init();

	// Format appropriate headers.
	struct curl_slist *chunk = NULL;
	chunk = curl_slist_append(chunk, key.c_str());
	chunk = curl_slist_append(chunk, sign.c_str());

	if (curl) {
		// First set the URL that is about to receive our POST. This URL can
		// just as well be a https: URL if that is what should receive the
		// data.
		curl_easy_setopt(curl, CURLOPT_POST, 1L);
		curl_easy_setopt(curl, CURLOPT_URL, trading_url.c_str());
                curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str());
		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

		// Perform the request, res will get the return code.
		res = curl_easy_perform(curl);
		// Check for errors.
		if (res != CURLE_OK)
			fprintf(stderr, "curl_easy_perform() failed: %s\n",
				curl_easy_strerror(res));

		// Always cleanup.
		curl_easy_cleanup(curl);
	}
	curl_global_cleanup();
}


main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <curl/curl.h>
#include <string>
#include <ctime>
#include <openssl/hmac.h>

#include "Poloniex.h"

int main()
{
	Poloniex poli("API_KEY", "API_SECRET");
	poli.queryBalances();

	system("pause");
	return 0;
}
Last edited on
Disclaimer: I have no knowledge of either OpenSSL or Poloniex.

Poloniex.cpp: line 25: Functions that return 'unsigned char *' usually return raw bytes, not ASCII strings. Are you sure you want to cast the return value of HMAC() to const char * and then append that to an std::string? What format does the API expect the Sign-Header header to be in?
Thanks for responding.

What's the difference between raw bytes and ASCII strings?
Aren't all data types essentially raw bytes?

The reason I casted from and unsigned char* to a char* was because
chunk = curl_slist_append(chunk, sign.c_str());
on line 43 requires a char*.

I was worried about this.
So to test it out (this isn't shown in the code) I printed to the standard output the value of buf1 and the value of buf2, and they have the exact same values (buf1 is an unsigned char* and buf2 is a const char*).
But it comes out as weird gibberish:
(e.g. ≥¶√&╢æ╟½♥cT╔?ì= τâIç;Ωs3[Vc╩╟╕#┌⌐√-↓*æ░#♀▒α\å├↓).
So I'm not sure if that's a problem either.

And I don't know what format the Sign-Header should be in because the Poloniex API documentation doesn't specify. So I'm assuming the default? If that's so, what is the default? And am I abiding to the rules?
Last edited on
Aren't all data types essentially raw bytes?
An object is an object. How they're represented in memory is an implementation detail.
On the other hand, an array of bytes is precisely that: an array of bytes.

What's the difference between raw bytes and ASCII strings?
For example:
 
const char ascii[] = "deadbeef";
ascii is an ASCII string with eight characters, representing a string of eight hexadecimal digits. ascii is null-terminated (like any valid C string) and can be printed without any special considerations. But
 
unsigned char data[] = { 0xDE, 0xAD, 0xBE, 0xEF };
is not. data is an array of four bytes. If you try to print data you'll get gibberish, like you did, because its bytes are not necessarily valid ASCII values, nor is it necessarily null-terminated.

Without knowing anything else, my first guess would be that Poloniex wants Sign-Header to contain a hex string of the HMAC. APIs that choose this form of encoding binary data as ASCII characters do so because hexadecimal is trivial to convert back and forth between byte values. The second-most-likely encoding is Base64, which could be chosen because binary data encoded in Base64 usually generates shorter strings than when encoded in hexadecimal.

So, in other words, you need to convert the array pointed to by the return value of HMAC into a hex string. Most likely the array is fixed-length and contains 64 bytes (since a SHA-512 digest contains 512 bits), so that means it will produce a 128 character hex string.
Ok I updated the code in Poloniex.cpp starting on line 30, converting it to a hex string, along with organizing how the request is built starting on line 55 to make it clearer.

I tried it out, but I'm still getting the same error {"error":"Invalid API key\/secret pair."}.

Do you think you could try it out? The only dependency needed is libcurl,
and mine came with openssl. You don't need an api_key or api_secret either.
It'll still get the same error.
1. You can't use strlen() to get the length of something that's not a string, such as the array returned by HMAC(). It's not a string. Quit casting it to const char *.
2. You don't even need to get the length, it's known in advance: 64 bytes.
3. The way you're using std::stringstream is wrong. Byte values < 0x10 will use a single character (0x00 => "0" instead of "00"). You need to use std::setw() and std::setfill().
4. It's been a while since the last time I used libcurl, but to me it looks like you're setting CURLOPT_HTTPHEADER wrong.
https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
I adjusted the code again.
I'm not sure how to go about what you're saying in step 3.

1) Got rid of strlen().
2) Replaced strlen() with 64.
3) Not sure what to do.
4) I prefixed the headers with "Key: " and "Sign: " for their respective headers.

The output I'm getting for the Sign looks more reasonable now (note changes every request because of increasing nonce (based on the time)):
1
2
Sign: e92349213bd871495adb41c327dcee19448b832f9e5beb15c26aeb8a101db6baabab91467d
4d5451912e93dddfdf35e4e5fcf50a0ff6e42282a6d66d72054


But I'm still getting the same error:
{"error":"Invalid API key\/secret pair."}

Here's the full output from curl in the console if this helps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
* STATE: INIT => CONNECT handle 0xe478b0; line 1418 (connection #-5000)
* Added connection 0. The cache now contains 1 members
* STATE: CONNECT => WAITRESOLVE handle 0xe478b0; line 1454 (connection #0)
*   Trying 104.20.12.48...
* TCP_NODELAY set
* STATE: WAITRESOLVE => WAITCONNECT handle 0xe478b0; line 1535 (connection #0)
* Connected to poloniex.com (104.20.12.48) port 443 (#0)
* STATE: WAITCONNECT => SENDPROTOCONNECT handle 0xe478b0; line 1587 (connection
#0)
* Marked for [keep alive]: HTTP default
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* STATE: SENDPROTOCONNECT => PROTOCONNECT handle 0xe478b0; line 1601 (connection
 #0)
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: OU=Domain Control Validated; CN=*.poloniex.com
*  start date: Dec  4 19:50:49 2015 GMT
*  expire date: Dec  4 19:50:49 2018 GMT
*  subjectAltName: host "poloniex.com" matched cert's "poloniex.com"
*  issuer: C=BE; O=GlobalSign nv-sa; CN=AlphaSSL CA - SHA256 - G2
*  SSL certificate verify result: unable to get local issuer certificate (20), c
ontinuing anyway.
* STATE: PROTOCONNECT => DO handle 0xe478b0; line 1622 (connection #0)
> POST /tradingApi HTTP/1.1
Host: poloniex.com
Accept: */*
Key: api_key (not telling you hehe)
Sign: c6982e14c721ce62163cf5b72adbffd5573b67865d97b719dd8deec65e180408ad2b4efbd8
dca8dbfc697b42a265aef2bf2b6b486cb786221a7e48b1b26
Content-Length: 40
Content-Type: application/x-www-form-urlencoded

* upload completely sent off: 40 out of 40 bytes
* STATE: DO => DO_DONE handle 0xe478b0; line 1684 (connection #0)
* STATE: DO_DONE => WAITPERFORM handle 0xe478b0; line 1809 (connection #0)
* STATE: WAITPERFORM => PERFORM handle 0xe478b0; line 1825 (connection #0)
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 403 Forbidden
< Date: Wed, 27 Dec 2017 03:43:24 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Set-Cookie: __cfduid=d4e750c47e072780ab7ac7afec92d4e1e1514346204; expires=Thu,
 27-Dec-18 03:43:24 GMT; path=/; domain=.poloniex.com; HttpOnly
< Cache-Control: private
* Server cloudflare-nginx is not blacklisted
< Server: cloudflare-nginx
< CF-RAY: 3d3946800fb6717f-ORD
<
{"error":"Invalid API key\/secret pair."}* STATE: PERFORM => DONE handle 0xe478b
0; line 1994 (connection #0)
* multi_done
* Connection #0 to host poloniex.com left intact
* Expire cleared
Press any key to continue . . . 
Last edited on
If anyone needs additional information I'd be happy to give it.
Topic archived. No new replies allowed.