Coldfusion CFHTTP with SHA512-hmac signed REST request body
Asked Answered
U

1

14

I am trying to make a signed request to the trading API at bitfloor.com (it's a REST API)

Bitfloor gives me:

1) API Key (i.e. 6bd2b780-00be-11e2-bde3-2837371c3c3a)

2) Secret Key (i.e. oaFz62YpmbWiXwseMUSod53D8pOjdyVcweNYdiab/TSQqxk6IuemDvimNaQoA==)

The following is Bitfloor's exact instructions for making the request:

Requests must be HTTPS POST requests on port 443 (https). Each request must contain the required headers (listed below). The headers identify, verify, and validate your request to prevent tampering. headers

bitfloor-key This is the provided by bitfloor to uniquely identify your account. (i.e. 6bd2b780-00be-11e2-bde3-2837371c3c3a)

bitfloor-sign The sign field is a sha512-hmac of the request body using the secret key which corresponds to your api key.

To sign your request: base64 decode the secret key into the raw bytes (64 bytes). Use those bytes for your sha512-hmac signing of the http request body. Base64 encode the signing result and send in this header field.

bitfloor-passphrase The passphrase you specified when creating this api key. We cannot recover your passphrase if forgotten. You will need to create a new API key.

bitfloor-version The api version of the resource you are interested in. The only valid value currently is 1


After a full eight hours of trial and error and searching the internet repeatedly for any sort of insight or information, the following code is as close as I can come to what I think might be somewhere in the direction of how to construct the request properly, alas, no matter what I attmept I get "Invalid Signature" returned by their API.

Here is what I have so far...

FIRST, I found this function on the web that someone wrote to do the SHA512 signing:

<cffunction name="HMAC_SHA512" returntype="binary" access="public" output="false">
    <cfargument name="signKey" type="string" required="true">
    <cfargument name="signMessage" type="string" required="true">

    <cfset var jMsg = JavaCast("string",arguments.signMessage).getBytes("iso-8859-1")>
    <cfset var jKey = JavaCast("string",arguments.signKey).getBytes("iso-8859-1")>
    <cfset var key  = createObject("java","javax.crypto.spec.SecretKeySpec")>
    <cfset var mac  = createObject("java","javax.crypto.Mac")>
    <cfset key  = key.init(jKey,"HmacSHA512")>
    <cfset mac  = mac.getInstance(key.getAlgorithm())>
    <cfset mac.init(key)>
    <cfset mac.update(jMsg)>
    <cfreturn mac.doFinal()>
</cffunction>

I have no idea what it does, but it seems to work and does so without error.

Here is my implementation of this function and my attempt at making the request: NOTE: The "nonce" value is a required param that must be sent with the request.

<cffunction name="myorders">
    <cfset nonce        = dateDiff("s",createDateTime(2012,01,01,0,0,0),now())>
    <cfset requestbody  = "?nonce=#nonce#">
    <cfset key      = "oaFz62YpmbWiXwseMUSod53D8pOjdyVcweNYdiab/TSQqxk6IuemDvimNaQoA==">
    <cfset sign     = HMAC_SHA512(key,requestbody)>
    <cfset signed       = binaryEncode(sign,"Base64")>

    <!--- HTTP REQUEST --->
    <cfhttp url = "https://api.bitfloor.com/orders#requestbody#"
        method  = "post"
        result  = "bitfloor">

    <!--- HEADERS --->
    <cfhttpparam
        type    = "body"
        value   = requestbody>
    <cfhttpparam
        type    = "header"
        name    = "bitfloor-key"
        value   = "6bd2b780-00be-11e2-bde3-2837371c3c3a">
    <cfhttpparam
        type    = "header"
        name    = "bitfloor-sign"
        value   = signed>
    <cfhttpparam
        type    = "header"
        name    = "bitfloor-passphrase"
        value   = "mysecretpassphrase">
    <cfhttpparam
        type    = "header"
        name    = "bitfloor-version"
        value   = "1">
    </cfhttp>
</cffunction>

I think most of my confusion comes from not knowing exactly what the "request body" is. I feel like I'm not signing the right thing perhaps.

I hope there is a Coldfusion programmer out there who is familiar with signed requests. I'm at my wit's end.

Please help! Namaste

Use answered 16/10, 2012 at 18:42 Comment(0)
B
2

I have not used that api, but I ran some tests and it seems to work with the following tweaks:

  • Since the secretKey value is base64 encoded, your signing function needs to use binaryDecode to properly extract the bytes. Using String.getBytes(...) produces a completely different (and wrong) result.

  • The expected request body value is just: nonce=#nonceValue# (without the leading "?")

  • It seems to require the Content-Type=application/x-www-form-urlencoded header, otherwise it fails to parse the content and the response is: {"error":"no nonce specified"}

Code

 <cfset apiKey = "6bd2b780-00be-11e2-bde3-2837371c3c3a">
 <cfset secretKey = "oaFz62YpmbWiXwseMUSod53D8pOjdyVcweNYdiab/TSQqxk6IuemDvimNaQoA==">
 <cfset passphrase = "your secret phrase">

 <cfset requestBody  = "nonce="& now().getTime()>
 <cfset signBytes    = HMAC_SHA512(secretKey, requestbody)>
 <cfset signBase64   = binaryEncode(signBytes, "base64")>

 <cfhttp url="https://api.bitfloor.com/orders" method="post" port="443" result="bitfloor">
    <cfhttpparam type="header" name="Content-Type" value="application/x-www-form-urlencoded">
    <cfhttpparam type="header" name="bitfloor-key" value="#apiKey#">
    <cfhttpparam type="header" name="bitfloor-sign" value="#signBase64#">
    <cfhttpparam type="header" name="bitfloor-passphrase" value="#passphrase#">
    <cfhttpparam type="header" name="bitfloor-version" value="1">
    <cfhttpparam type="body" value="#requestBody#">
 </cfhttp>

 <cfdump var="#bitfloor#" label="Response">

<cffunction name="HMAC_SHA512" returntype="binary" access="public" output="false">
    <cfargument name="base64Key" type="string" required="true">
    <cfargument name="signMessage" type="string" required="true">
    <cfargument name="encoding" type="string" default="UTF-8">

     <cfset var messageBytes = JavaCast("string",arguments.signMessage).getBytes(arguments.encoding)>
     <cfset var keyBytes = binaryDecode(arguments.base64Key, "base64")>
     <cfset var key  = createObject("java","javax.crypto.spec.SecretKeySpec")>
     <cfset var mac  = createObject("java","javax.crypto.Mac")>
     <cfset key  = key.init(keyBytes,"HmacSHA512")>
     <cfset mac  = mac.getInstance(key.getAlgorithm())>
     <cfset mac.init(key)>
     <cfset mac.update(messageBytes)>

     <cfreturn mac.doFinal()>
</cffunction>
Bazluke answered 17/10, 2012 at 0:30 Comment(3)
Thank you so much Leigh! I wish I could explain to you the elation I felt when I saw real data returned finally after hundreds of times of failure. It seemed like it would never work. You are a real life saver! Thank you Thank you Thank you! I'm going to stick around on this web site and try my best to pay it forward. Namaste my friendUse
Just came here to give you points for answering such a difficult question.Lavenialaver
@KRC - Thanks. It is too bad api's often underestimate the value of a few "concrete" examples. It is much easier for developers to spot problems and logic errors (like with the hmacSHA512 function) when they know exactly what the "correct" input looks like.Bazluke

© 2022 - 2024 — McMap. All rights reserved.