1
votes

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?

1
How are you generating the key?Leigh
We are using the oauth library oauth.riaforge.org. Note that in my example I have blocked out the bulk of the key with "x"suser460114
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 describe one method and another approach is shown here.Leigh
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
No, according to their API 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

1 Answers

1
votes

(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.