1

I'm writing a wrapper for the Xero API, using oAuth and two-legged authentication. It is a "private" application, as Xero calls it, which requires a RSA-SHA1 signature. The Coldfusion oAuth wrapper doesn't have a function for encrypting in RSA-SHA1, only HMAC-SHA1. We are on CF9.

Consequently, in the course of running a GET request, I get the following error:

signature_method_rejected
Private applications must use the RSA-SHA1 signature method

So, it looks like the GET call is working, but the issue is with the signature method. I found what I thought looked like the solution someone created, as follows:

<cffunction name="rsa_sha1" returntype="string" access="public" descrition="RSA-SHA1 computation based on supplied private key and supplied base signature string.">
               <cfargument name="signKey" type="string" required="true" hint="base64 formatted PKCS8 private key">
               <cfargument name="signMessage" type="string" required="true" hint="msg to sign">
               <cfargument name="sFormat" type="string" required="false" default="UTF-8">

               <cfset var jKey = JavaCast("string", arguments.signKey)>
               <cfset var jMsg = JavaCast("string",arguments.signMessage).getBytes(arguments.sFormat)>

               <cfset var key = createObject("java", "java.security.PrivateKey")>
               <cfset var keySpec = createObject("java","java.security.spec.PKCS8EncodedKeySpec")>
               <cfset var keyFactory = createObject("java","java.security.KeyFactory")>
               <cfset var b64dec = createObject("java", "sun.misc.BASE64Decoder")>

               <cfset var sig = createObject("java", "java.security.Signature")>

               <cfset var byteClass = createObject("java", "java.lang.Class")>
               <cfset var byteArray = createObject("java","java.lang.reflect.Array")>

               <cfset byteClass = byteClass.forName(JavaCast("string","java.lang.Byte"))>
               <cfset keyBytes = byteArray.newInstance(byteClass, JavaCast("int","1024"))>
               <cfset keyBytes = b64dec.decodeBuffer(jKey)>
               <!--- keyBytes = 48-111-10345-125-5349-114-581835-28-330-3984120-2848-4384-1-43 --->

               <cfset sig = sig.getInstance("SHA1withRSA", "SunJSSE")>
<!--- error occurs on the line below --->
               <cfset sig.initSign(keyFactory.getInstance("RSA").generatePrivate(keySpec.init(keyBytes)))> 
               <cfset sig.update(jMsg)>
               <cfset signBytes = sig.sign()>

               <cfreturn ToBase64(signBytes)>
         </cffunction>

It receives the following arguments:

SFORMAT     UTF-8
SIGNKEY     0JxxxxxxxxxxxxxxxxxxxP&
SIGNMESSAGE     GET&https%3A%2F%2Fapi.xero.com%2Fapi.xro%2F2.0%2FContacts&contactid%3D%26contactnumber%3D%26name%3D7-Eleven%26oauth_consumer_key%3Dxxxxxxxxxxxxxxxx%26oauth_nonce%3Dxxxxxxxxxxxxxxxx%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1339055484%26oauth_version%3D1.0 

However, this produces the following error:

Could not read BER data.(ASN1Lengths.determineLengthLen: length greater than 0x7FFF,FFFF.)

ColdFusion cannot determine the line of the template that caused this error. This is often caused by an error in the exception handling subsystem. 

Can anyone could shed any light on this?

EDIT ----

enter image description here

There is a link for uploading the public cert, which I did. THere is also a note saying: "Note, For Private applications, the consumer token and secret are also used as the access token and secret.". So I am assuming I need the "Consumer Secret" value shown there in order to sign the request. That being the case, how do I convert that secret key value to RSA-SH1 format for the signature?

user460114
  • 1,848
  • 3
  • 31
  • 54
  • How are you generating the key? – Leigh Jun 07 '12 at 19:02
  • We are using the oauth library http://oauth.riaforge.org/. Note that in my example I have blocked out the bulk of the key with "x"s – user460114 Jun 07 '12 at 19:52
  • I believe you need to generate your own `RSA` private key. Then feed the "base64 formatted PKCS8 private key" string into the function above. The [Google Oauth docs](https://developers.google.com/gdata/docs/auth/oauth#GeneratingKeyCert) describe one method and another approach is [shown here](http://www.houseoffusion.com/groups/cf-talk/thread.cfm/threadid:62277). – Leigh Jun 07 '12 at 21:31
  • In other words, the function above is not going to work with the HMAC-SHA1 value. You need an `RSA` private key, encoded in a specific way. If you have that already, I believe the code is relatively simple. – Leigh Jun 08 '12 at 03:34
  • The bouncycastle solution seems to be for generating a random key pair. I've tried it and it does that successfully. I think I need to use my allocated secret key and generate the base64 formatted PKCS8 version from that, rather than a random key. The openssl version seems to require access to the server and IIS, which I don't have unfortunately. Given my pre-generated secret key, e.g. 0JGZLYPLMXXXXXXXXXXA2VR45DDVVP, which xero is looking for, I guess I'm still hazy about how to generate RSA version. Is anyone available for hire to help me with this:-) – user460114 Jun 08 '12 at 12:53
  • 1
    No, according to [their API](http://blog.xero.com/developer/api-overview/setup-an-application/#private-apps) private applications do not use the secret key for signing. You must generate an `RSA` public/private pair (one time event). Then upload your *public* certificate to their server. They will use it to verify your requests were signed by you and not someone else. Then you use your private key to sign all requests using `RSA-SHA1`. – Leigh Jun 08 '12 at 16:14
  • Thanks for bearing with me on this. See the edit in the OP. I'm obviously still not understanding something. – user460114 Jun 08 '12 at 21:10
  • Feel free to make your comments into an answer. It's the answer. Not your fault I'm slow on the uptake. Besides, we're running out o comment quota. – user460114 Jun 08 '12 at 21:23
  • Sorry for the delay, got caught up with work. I can post an example of how sign a message with an RSA private in PKCS8 format, using your function above. However, I am not familiar Xero. So there may be a little more to it. – Leigh Jun 09 '12 at 18:31
  • Yes, please do:-) I'm sure I can fill in the gaps. Can you create an answer and post it within that? – user460114 Jun 10 '12 at 00:43
  • Just a quick heads up. If you do not take any action on the bounty, your points will disappear back into the void [according to the faq's](http://stackoverflow.com/faq#bounty) ;-) – Leigh Jun 15 '12 at 19:16

1 Answers1

1

(Summary from the comments)

According to their API private applications must generate an RSA public/private pair (one time event). You then upload your public certificate to their server, and use the private key to sign all requests using RSA-SHA1. If you followed the instructions in their link your private key will be in PEM format, which is just the key value encoded in base64, enclosed within a BEGIN/END wrapper:

    -----BEGIN RSA PRIVATE KEY-----
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ....
    -----END RSA PRIVATE KEY-----

You need to extract the portion between the wrapper before using the key with PKCS8EncodedKeySpec. There are at least three ways to do that. The simplest option is to use string functions:

    // read in the  key file and remove the wrapper
    pathToKey = "c:/path/to/file/privateKey.pem";
    rawKey = FileRead( pathToKey );
    rawKey = replace( rawKey, "-----BEGIN RSA PRIVATE KEY-----"& chr(10), "" );
    rawKey = replace( rawKey, "-----END RSA PRIVATE KEY-----", "" );

    yourMessage = "GET&https%3A%2F%2Fapi.xero.com%2Fapi.xro%2F2.0%2F...";
    signature = rsa_sha1( rawKey, yourMessage, "utf-8" );

Another option is to use the BouncyCastle PEMReader class. It does it all for you (and also supports password protected keys).

    pathToKey  = "c:/path/to/file/privateKey.pem";
    provider   = createObject("java", "org.bouncycastle.jce.provider.BouncyCastleProvider").init();
    security   = createObject("java", "java.security.Security").addProvider( provider );
    fileReader = createObject("java", "java.io.FileReader").init( pathToKey );
    keyReader  = createObject("java", "org.bouncycastle.openssl.PEMReader").init( fileReader);
    privateKey = keyReader.readObject().getPrivate();
    rawKey     = binaryEncode( privateKey.getEncoded(), "base64" );
    yourMessage = "GET&https%3A%2F%2Fapi.xero.com%2Fapi.xro%2F2.0%2FContacts&....";
    signature  = rsa_sha1( rawKey, yourMessage, "utf-8" );

Yet another option is to convert the key to DER format with openssl

    $ openssl pkcs8 -topk8 -in privateKey.pem -outform DER -nocrypt -out privateKey.pk8

    pathToKey = "c:/path/to/file/privateKey.pk8";
    bytes = binaryEncode( fileReadBinary(pathToKey), "base64");
    yourMessage = "GET&https%3A%2F%2Fapi.xero.com%2Fapi.xro%2F2.0%2F...";
    signature = rsa_sha1(rawKey, testMessage, "utf-8");

Note If you posted your actual private key here (or on another forum), it is compromised. So I would strongly recommend generating new keys.

Community
  • 1
  • 1
Leigh
  • 28,765
  • 10
  • 55
  • 103