While trying to integrate QuickBooks Merchant Services (QBMS) into an ASP.NET web application, I immediately ran into a stumbling block. QBMS requires the application to pass an SSL client certificate with each service call. The Intuit Developer Network (IDN) has documentation on how to create, install, and use the certificate for various systems. After reading the IDN documentation for doing this on IIS with ASP.NET (it’s called “Climbing the Mountain” http://developer.intuit.com/developer/newsletter.asp?id=350) I was not only very overwhelmed, I was stuck. My server is controlled by my hosting service and I have no access to the IIS configuration or the key store for certificates.
I had already gone through this with PayPal, so I know that ASP.NET 2.0 has the facility to work with certificates stored in a normal file in the server’s file system. On PayPal, it was a bit easier, because they supply a signed certificate in a single step that doesn’t require creating a certificate request. I’ll explain this in detail, but here is a synopsis of the required steps:
- Create a private key.
- Create a certificate request.
- Post the certificate request in the IDN site to have it signed.
- Combine the private key and signed certificate into an encrypted file.
- Modify your application to use the new encrypted certificate file.
1. You need a utility for creating keys, encrypting certificates, etc. I use an open source program called OpenSSL (http://www.openssl.org) and these instructions assume you are using that program. You can download the Windows version here: http://gnuwin32.sourceforge.net/packages/openssl.htm.
To create the private key, you need to have some random bytes of data. OpenSSL can do that by setting this environment variable and running this command: (randomdata.rnd is an arbitrary file name I chose.)
This command creates the private key: (Again, privkey.pem is an arbitrary file name.)
2. The certificate request has to be in a specific format for QBMS. OpenSSL can use a configuration file to make setting the properties easier. Here is a sample configuration file: (I arbitrarily named it openssl.cnf.)
[ req ]
default_bits = 1024
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
prompt = no
[ req_distinguished_name ]
C = US
ST = CA
L = San Francisco
O = My Company
OU = My Department
CN = myServer.myCompany.com:myApplicationName
#emailAddress = myName@myCompany.com
[ req_attributes ]
challengePassword = A challenge password
The most important property is CN. To comply with QBMS, this has to be in the <server name>:<application name> format, where <application name> is the name you registered on IDN.
Here is a problem I ran into: The server name must match a reverse lookup on its IP address. In my case, it didn’t and QBMS would not accept the certificate. To fix this, I did an nslookup on my server name to get the IP address. Then I did an nslookup on the IP address and used the name returned there for <server name>.
The certificate request is created with this command: (cert_req.pem is an arbitrary file name.)
3. To get the signed certificate from IDN, go here: http://appreg.intuit.com. Follow the steps to register your application, or if you have already registered your application, select it, and click the "Sign Client Cert for Selected" button. When you get to the page asking, "Server name that will POST to Intuit services," enter the name that you used above for <server name>.
The next page will ask you to, "Please paste your Certificate Signing Request (CSR) here." Paste the text from the cert_req.pem file created at the end of step 2. If QBMS likes your request, it will return a signed certificate. Copy the returned text and save it in a text file. For these instructions, I chose the name intuit_cert.pem.
4. When you combine the private key and signed certificate into an encrypted file, it will prompt you to select an export password. The password you select will be used in your application when reading the encrypted certificate file in order to pass it to QBMS. Here is the command to combine and encrypt the private key and signed certificate: (intuit_cert.p12 is an arbitrary name I chose for the final, encrypted certificate file.)
5. Store the intuit_cert.p12 file in one of the folders within your web application. IDN supplies the source code for a nice wrapper that simplifies calling the QBMS API from ASP.NET. I’ll show here how to modify that wrapper to use the intuit_cert.p12 file. You can find this wrapper here: http://developer.intuit.com/qbms/integration_center/?id=1350. It consists of two class library projects named QBMSLib and IDNRequestor.
In the IDNRequestor library, is a class named CallIntuitWebApps. Here is the original code for that class:
public class CallIntuitWebApps:ServicedComponent
{
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
private extern static bool DuplicateToken(IntPtr ExistingTokenHandle,
int SECURITY_IMPERSONATION_LEVEL,
ref IntPtr DuplicateTokenHandle);
[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
private extern static bool CloseHandle(IntPtr handle);
public string IntuitPostRequestWithClientCert(string certFile,string URL, string contenttype, string postData)
{
EventLog log = new EventLog("Application");
log.Source = "IDNRequestor";
bool retVal = false;
const int SecurityImpersonation = 2;
IntPtr dupeTokenHandle = DupeToken(WindowsIdentity.GetCurrent().Token, SecurityImpersonation);
if (IntPtr.Zero == dupeTokenHandle)
{
throw new Exception("Unable to duplicate token");
}
//Load profile with client cert installed
ProfileManager.PROFILEINFO profile = new ProfileManager.PROFILEINFO();
profile.dwSize = 32;
profile.lpUserName = @"IDN\SERVICED";
retVal = ProfileManager.LoadUserProfile(dupeTokenHandle, ref profile);
if (false == retVal)
{
throw new Exception("Error loading user profile. " + Marshal.GetLastWin32Error());
}
log.WriteEntry("Loading certificates");
System.Net.HttpWebRequest req = (HttpWebRequest)WebRequest.Create(URL);
X509Certificate cert = X509Certificate.CreateFromCertFile(certFile);
log.WriteEntry("Loaded cert Hash:" + cert.GetCertHashString());
req.ClientCertificates.Add(cert);
ASCIIEncoding encoding = new ASCIIEncoding();
byte[] data = encoding.GetBytes(postData);
req.Method = "POST";
req.ContentLength = data.Length;
req.ContentType = contenttype;
System.IO.Stream reqStream = req.GetRequestStream();
reqStream.Write(data,0,data.Length);
reqStream.Close();
System.Net.HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
if (req.HaveResponse)
{
if (HttpStatusCode.OK == resp.StatusCode || HttpStatusCode.Accepted == resp.StatusCode)
{
System.IO.StreamReader respReader = new System.IO.StreamReader(resp.GetResponseStream());
string respString = respReader.ReadToEnd();
ProfileManager.UnloadUserProfile(WindowsIdentity.GetCurrent().Token,profile.hProfile);
CloseHandle(dupeTokenHandle);
return respString;
}
else
{
throw new Exception("Request failed: " + resp.StatusDescription);
}
}
else
{
throw new Exception("Could not get response from Intuit server");
}
}
private IntPtr DupeToken(IntPtr token, int Level)
{
IntPtr dupeTokenHandle = new IntPtr(0);
bool retVal = DuplicateToken(token,Level,ref dupeTokenHandle);
if (false == retVal)
{
return IntPtr.Zero;
}
return dupeTokenHandle;
}
}
This class impersonates a user with access to the key store in order to load the certificate from there. We’ll remove that section and replace it with code that loads the certificate from the file system. Here is the modified class:
public class CallIntuitWebApps
{
public string IntuitPostRequestWithClientCert(string certFile, string password,
string URL, string contenttype, string postData)
{
System.Net.HttpWebRequest req = (HttpWebRequest)WebRequest.Create(URL);
try
{
req.ClientCertificates.Add(new X509Certificate2(certFile, password));
}
catch
{
throw new Exception("Invalid ssl cert: " + certFile);
}
ASCIIEncoding encoding = new ASCIIEncoding();
byte[] data = encoding.GetBytes(postData);
req.Method = "POST";
req.ContentLength = data.Length;
req.ContentType = contenttype;
System.IO.Stream reqStream = req.GetRequestStream();
reqStream.Write(data,0,data.Length);
reqStream.Close();
System.Net.HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
if (req.HaveResponse)
{
if (HttpStatusCode.OK == resp.StatusCode || HttpStatusCode.Accepted == resp.StatusCode)
{
System.IO.StreamReader respReader = new System.IO.StreamReader(resp.GetResponseStream());
string respString = respReader.ReadToEnd();
return respString;
}
else
{
throw new Exception("Request failed: " + resp.StatusDescription);
}
}
else
{
throw new Exception("Could not get response from Intuit server");
}
}
}
The password argument we added to the IntuitPostRequestWithClientCert function is the password we used when encrypting the certificate in step 4, above. IDN has a sample web application named QBMSDonorSample that can also be found here: http://developer.intuit.com/qbms/integration_center/?id=1350. This application shows how to use the QBMSLib and IDNRequestor libraries.
As an overly simplified example, here is the code behind file for a web form that attempts to post a credit card payment. The intuit_cert.p12 file created in step 4 is stored in the root folder of the site in this example. The WebRequestSender class implements the RequestSender interface defined in the QBMSLib library.
using System;
using Intuit.QBMSLib;
using IDNRequestor;
public partial class PostTest : System.Web.UI.Page
{
protected void Button1_Click(object sender, EventArgs e)
{
WebRequestSender mySender =
new WebRequestSender(Page.Server.MapPath("~/intuit_cert.p12"), "password");
QBMSRequestor requestor =
new QBMSRequestor(QBMSAppType.Hosted, "application name", "connection ticket",
string.Empty, "English", "application ID", "1.0", mySender, true);
ChargeResponse response =
requestor.SendChargeRequest(System.Guid.NewGuid().ToString(), "4111111111111111",
"12", "2008", false, "10.00", "User Name", "User Address", "ZIP", "", "", "CVV",
true, false);
}
private class WebRequestSender : RequestSender
{
private string myCertFile;
private string myPassword;
public WebRequestSender(string certFile, string password)
{
myCertFile = certFile;
myPassword = password;
}
public string SendRequest(string URL, string request)
{
CallIntuitWebApps caller = new CallIntuitWebApps();
return caller.IntuitPostRequestWithClientCert(myCertFile, myPassword, URL,
"application/x-qbmsxml", request);
}
}
}