HMAC-SHA512 and SHA256


#1

Hello, I’m trying to make a API binding for an exchange named kraken(https://www.kraken.com/), I already have figured out how to call it’s public methods(ie. not user-specific) but to access private methods I need to put an API-Sign in the header, which needs to be the following:

API-Sign = Message signature using HMAC-SHA512 of (URI path + SHA256(nonce + POST data)) and base64 decoded secret API key

I have an API key and a secret API key from the exchange I can use, and the nonce seems pretty straightforward(a value that increases every subsequent call). But I have zero experience with encryption stuff, and I have no idea what most of this actually means, I looked at a python binding someone else made (https://github.com/veox/python3-krakenex/blob/master/krakenex/api.py) and at this question for javascript (https://stackoverflow.com/questions/42784299/kraken-com-api-generate-the-message-signature-in-angularjs), but I don’t really understand what they are doing and am at a complete loss on how to implement it in rust. I looked at rust-crypto (https://github.com/DaGenix/rust-crypto/blob/master/src/hmac.rs), but it appears to be discontinued, RustCrypto (https://github.com/RustCrypto/hashes#crate-names), which aims to fill that gap doesn’t seem to support HMAC.

Can someone point me in the right direction on how to do this? I don’t need it to be in rust per se but it has to be reasonably fast (so I’d rather not run a python script to do it, but a c-binding would be fine I guess).

This is the page where it describes the API calls.


#2

The RustCrypto project does have an HMAC implementation available: https://docs.rs/hmac/0.4.2/hmac/. Here’s an example of use: https://github.com/sfackler/rust-postgres/blob/master/postgres-protocol/src/authentication/sasl.rs#L45-L47


#3

Ohh thanks! I only looked at the list of supported algorithms on their readme, and assumed they didn’t have it because it wasn’t there. I’ll try with RustCrypto’s hmac then, and see how far I get.


#4

There’s an example for how to do something very similar to this in the ring documentation: https://briansmith.org/rustdoc/ring/hmac/#using-the-multi-part-api.

Use the base64 crate (or similar) to decode the secret API key and use the decoded value as key_value.


#5

@alvitawa
RustCrypto project splits algorithms into thematic repositories, so hmac resides in the RustCrypto/MACs. Feel free to ask questions if you’ll encounter any difficulties!


#6

This is what I managed to plough together:

Function to get the signature:

pub fn get_api_sign<'a>(&self, uripath: &mut Vec<u8>, postdata: &'a [u8]) -> MacResult<<Sha512 as digest::FixedOutput>::OutputSize> {
	let mut nonce_data_hasher = Sha256::default();

	let mut nonce_data_str = self.nonce_handler.get_nonce().to_string().into_bytes();
	nonce_data_str.extend(postdata);

	nonce_data_hasher.input(&nonce_data_str);

	let nonce_data_hash = nonce_data_hasher.result();

	//extend uripath now, shrink it later
	uripath.extend(nonce_data_hash);

	let mut sig_mac = Hmac::<Sha512>::new(self.api_secret_key);
	sig_mac.input(uripath); //variable uripath is really uripath + nonce_data_hash here

	let sig_result = sig_mac.result();

	// let sig_bytes = sig_result.code();

	let trunc_len = uripath.len() - nonce_data_hash.len();
	uripath.truncate(trunc_len); //remove the nonce_data_hash part from uripath to end with the same uripath

	sig_result
}

It takes Vec to do the concatenation more efficiently.

self.api_secret_key is and self.api_key are &'static [u8]

And then make a hyper request with it:

	let body = "";

	let sign_mac = self.get_api_sign(&mut req_balance_vec, body.as_bytes());
	let sign_code = sign_mac.code();

	println!("{:?}", sign_code);

	let sign = str::from_utf8(sign_code).expect("Couldn't convert sign code to str.");
	let api_key = unsafe { str::from_utf8_unchecked(self.api_key) };

	println!("{:?}", sign);
	println!("{:?}", api_key);

	let mut headers = Headers::new();
	headers.set(ApiKeyHeader::new(api_key));
	// headers.set_raw("API-Sign", sign); //For some reason resulted in a broken pipe error (no idea what that is)
	headers.set(ApiSignHeader(sign.to_owned()));


	let request = self.client.request(Method::Post, REQ_ACCOUNT_BALANCE).expect("Couldn't make request builder.")
		.headers(headers).body(body).build()

	match self.client.execute(request).map_err(|e| SimpleError::new(format!("Exec: {}", e))){
		Ok(mut v) => {
			let json: Value = v.json().expect("Couldn't extract json.");
			println!("XJson: {}", format!("{:?}", json));
		},
		Err(e) => println!("XErr: {:?}", e)
	}

But I get a panic when converting sign_code to a string I can use as vale for ApiSignHeader (.expect(“Couldn’t convert sign code to str.”) triggers).

Setting the sign header with headers.set_raw("API-Sign", sign_code); instead of by converting it to a string first, gives me a broken pipe error.

It seems like the get_signature() method is returning some bytes that do not represent valid characters, I’m unshure whether that’s supposed to happen or not.

What am I doing wrong? Or what step am I missing?


#7

You’re attempting to interpret the hash value as a UTF-8 encoded string, that might occasionally work but will mostly fail as it will contain invalid UTF-8 characters (and is almost certainly not what the server is expecting).

The Kraken API docs don’t actually specify what encoding API-Sign should be in, but from this line of python3-krakenex you can see that they base64 encode it. So instead of

let sign = str::from_utf8(sign_code).expect("Couldn't convert sign code to str.");

you probably want

let sign = base64::encode(sign_code);

(assuming you’re using the base64 crate).


#8

Looks like coinnect has an implementation of what you are looking for.


#9

Thanks! I think I also hadn’t decoded the base64 key, this is the updated code (restructured):

//with correct keys
pub static APIKEY: &'static str = "key"; 
pub static APISECRET: &'static [u8] = b"secret";
#[test]
	fn test_private_request() {

		let link = "https://api.kraken.com/0/private/Balance";
		let nonce = 1;
		let postdata = b"";

		let mut nonce_data_hasher = Sha256::default();

		let mut nonce_data_str = nonce.to_string().into_bytes();
		nonce_data_str.extend(postdata);

		nonce_data_hasher.input(&nonce_data_str);

		let nonce_data_hash = nonce_data_hasher.result();

		let mut uripath: Vec<u8> = link.as_bytes().iter().cloned().collect();

		uripath.extend(nonce_data_hash);

		let b64_decoded_api_secret_key = base64::decode(APISECRET).expect("Couldn't decode secret key.");

		let mut sig_mac = Hmac::<Sha512>::new(&b64_decoded_api_secret_key);
		sig_mac.input(&uripath); //variable uripath is really uripath + nonce_data_hash here

		let sig_result = sig_mac.result();

		let b64_encoded_sig = base64::encode(sig_result.code());

		let body = "";

		let mut headers = Headers::new();
		headers.set(ApiKeyHeader::new(APIKEY));
		headers.set(ApiSignHeader(b64_encoded_sig));

		let client = reqwest::Client::new().expect("Couldn't make client.");

		let request = client.request(Method::Post, link).expect("Couldn't make request builder.")
		.headers(headers).body(body).build();

		match client.execute(request) {
			Ok(mut v) => {
				let json: Value = v.json().expect("Couldn't extract json.");
				println!("XJson: {}", format!("{:?}", json));
			},
			Err(e) => println!("Req error: {:?}", e)
		}
	}

It’s printing this:
XJson: Object({“error”: Array([String(“EGeneral:Internal error”)])})

If I change a letter from the secret key (for example: an h to an i) I get the same error.

(Doing the same request using krakenex works fine)


#10

Oh wow, i’ll have a look at that then, I should have done a more thorough search I suppose.