ColdFusion : CoinBase API : CB-ACCESS-SIGN ERROR

251 Views Asked by At

I am almost there for getting the CoinBase API Sorted - but missing something or a bad conversion. I have tried a few things. This is best I can get too for building everything. I even setup a brand new key, in case old key.

I get a response from CoinBase : {"message":"invalid signature"} If I purposely error out Key, Passphrase, Timestampe : it identifies those as incorrect. So all is right except this signature bit. I've included the Coinbase API Signature Details as well.

 <cfset base_api = "https://api.exchange.coinbase.com/orders">
 <cfset req_path = "/orders">

 <cfset cb_key = "9999ac99999e6e99ee10dfd8ea5f9999">
 <cfset cb_s = "999a7S/JZVX09EAX/LvWz9999/ALnVQptso999999dxrVfXfd999993OXAlfdPGwGZUKPBa99999pg1ubhVlsw==">

 <cfset cb_pass = "999957agr99">

 <cfset pair = "SHIB-USDT">
 <cfset side = "sell">
 <cfset type = "market">
 <cfset size = "79940">

 <cfset startDate = createdatetime( '1970','01','01','00','00','00' )> 
 <cfset datetimeNow = dateConvert( "local2Utc", now() )>
 <cfset gmtnow = #DateAdd("h", 7, datetimeNow)#>
 <cfset UnixStamp = datediff( 's', startdate, gmtnow )>

 <cfscript>
     cbs = #cb_s#;
     body = SerializeJSON({
     size: '#size#',
     type: '#type#',
     side: '#side#',
     product_id: '#pair#'});
     method = 'POST';

     // create the prehash string by concatenating required parts
     message = #UnixStamp# & method & #req_path# & body;

     // decode the base64 secret
     key = toBinary( #cb_s# );

     // create sha256 hmac with the key
     theHmac = hmac("#message#","#Key#", "HMACSHA256");
    
     // encode the result
     cas = toBase64(theHmac);

 </cfscript>
     
 <cfoutput>

 <br>#cbs#<br>

 <b>#cas#</b><br><br>
 <br>#message#

 </cfoutput> 


 <cfhttp url="#base_api#" method="post" result="result" charset="utf-8"> 
 <cfhttpparam type="formfield" name="product_id" value="#pair#"> 
 <cfhttpparam type="formfield" name="side" value="#side#"> 
 <cfhttpparam type="formfield" name="type" value="#type#"> 
 <cfhttpparam type="formfield" name="size" value="#size#">

 <cfhttpparam type="header" name="CB-ACCESS-KEY" value="#cb_key#"> 
 <cfhttpparam type="header" name="CB-ACCESS-PASSPHRASE" value="#cb_pass#"> 
 <cfhttpparam type="header" name="CB-ACCESS-SIGN" value="#cas#"> 
 <cfhttpparam type="header" name="CB-ACCESS-TIMESTAMP" value="#unixstamp#"> 

 </cfhttp> 

This is from COINBASE API Documentation

Signing a Message

The CB-ACCESS-SIGN header is generated by creating a sha256 HMAC using the base64-decoded secret key on the prehash string timestamp + method + requestPath + body (where + represents string concatenation) and base64-encode the output. The timestamp value is the same as the CB-ACCESS-TIMESTAMP header.

The body is the request body string or omitted if there is no request body (typically for GET requests).

The method should be UPPER CASE.

Remember to first base64-decode the alphanumeric secret string (resulting in 64 bytes) before using it as the key for HMAC. Also, base64-encode the digest output before sending in the header.

 var crypto = require('crypto');

 var cb_access_timestamp = Date.now() / 1000; // in ms
 var cb_access_passphrase = '...';
 var secret = 'PYPd1Hv4J6/7x...';
 var requestPath = '/orders';
 var body = JSON.stringify({
     price: '1.0',
     size: '1.0',
     side: 'buy',
     product_id: 'BTC-USD'
 });
 var method = 'POST';

 // create the prehash string by concatenating required parts
 var message = cb_access_timestamp + method + requestPath + body;

 // decode the base64 secret
 var key = Buffer(secret, 'base64');

 // create a sha256 hmac with the secret
 var hmac = crypto.createHmac('sha256', key);

 // sign the require message with the hmac
 // and finally base64 encode the result
 var cb_access_sign = hmac.update(message).digest('base64');
1

There are 1 best solutions below

1
SOS On

I haven't used that API and unfortunately their documentation is sorely lacking any concrete examples of a generated signature. However, a good starting point would be to run the NodeJS example and output the results. Then attempt to match them with CF.

If you're not running NodeJS locally, use an online tool like JDoodle. For testing purposes, use a static timestamp and a valid base54 string for the secret. Output the generated values at the end of the script.

var crypto = require('crypto');

var cb_access_timestamp = 1645506818.784; 
var secret = 'c2VjcmV0IHZhbHVl'; //"secret value" in plain text
// ... rest of code ...
console.log("cb_access_timestamp=", cb_access_timestamp);
console.log("secret=", secret);
console.log("requestPath=", requestPath);
console.log("body=", body);
console.log("method=", method);
console.log("body=", body);
console.log("cb_access_sign="+ cb_access_sign);  

Next try and replicate those results in CF. Starting with the same static values

cb_access_timestamp = 1645506818.784; 
secret = "c2VjcmV0IHZhbHVl"; // "secret value" in plain text
requestPath = '/orders';
method = 'POST';

Take special care when constructing json strings. Always assume API's are case sensitive, unlike CF. Your current code produces a json string with all upper case key names, (unlike the NodeJS example). To preserve the correct case, enclose the key names in quotes.

// use "[" and "]" to create ordered structure 
bodyStruct = [
    "price" : "1.0",
    "size" : "1.0",
    "side" : "buy",
    "product_id" : "BTC-USD"
];

Also note that CF will be overly helpful and try and guess the data types of the structure values when serializing. You may need to use setMetaData() to force keys like price and size to be treated as strings, instead of numbers:

bodyStruct.setMetaData({
   "price": {type : "string"}
   , "size" : {type : "string"}
});

body = serializeJSON( bodyStruct );

Finally, don't get tripped up by encodings when converting the hmac value into base64. The current code passes the HMAC() result into ToBase64(). That produces the wrong result because ToBase64() can only convert plain strings (ascii, utf8, etc..) and HMAC() returns a hexadecimal encoded string. Instead use BinaryDecode() and BinaryEncode() to convert the hmac from hex to base64.

message = cb_access_timestamp & method & requestPath & body;
key = secret.binaryDecode("base64");
hmacHex = hmac( message, key, "HMACSHA256");
cb_access_sign = binaryEncode(hmacHex.binaryDecode("hex"), "base64");

If you output the CF results, they should match those from the NodeJS example.

// Results
writeDump({
   "cb_access_timestamp":  cb_access_timestamp
   , "secret" : secret
   , "requestPath" :  requestPath
   , "body" : body
   , "method" : method
   , "body" : body
   , "cb_access_sign" : cb_access_sign
});

NodeJS Results:

cb_access_timestamp = 1645506818.784
secret = c2VjcmV0IHZhbHVl
requestPath = /orders
body = {"price":"1.0","size":"1.0","side":"buy","product_id":"BTC-USD"}
method = POST
body = {"price":"1.0","size":"1.0","side":"buy","product_id":"BTC-USD"}
cb_access_sign = neoPZ9ZKzQKoPGCmOuUVRY9v1NOpADQlOF+TZi2W8Qc=

CF Results:

Variable Value
body {"price":"1.0","size":"1.0","side":"buy","product_id":"BTC-USD"}
cb_access_sign neoPZ9ZKzQKoPGCmOuUVRY9v1NOpADQlOF+TZi2W8Qc=
cb_access_timestamp 1645506818.784
method POST
requestPath /orders
secret c2VjcmV0IHZhbHVl

If the results match, update the timestamp and secret and try passing the request parameters (size, type, product_id, side) in the request body, not separately as type="formfield"

cfhttp(url="#base_api#", method="post", result="result", charset="utf-8") {
   cfhttpparam( type="header", name="CB-ACCESS-KEY", value="#cb_access_key#");
   cfhttpparam( type="header", name="CB-ACCESS-PASSPHRASE", value="#cb_access_passphrase#");
   cfhttpparam( type="header", name="CB-ACCESS-SIGN", value="#cb_access_sign#"); 
   cfhttpparam( type="header", name="CB-ACCESS-TIMESTAMP", value="#cb_access_timestamp#");
   cfhttpparam( type="header", name="Content-Type", value="application/json");

   cfhttpparam( type="body", value="#body#");
}