Recently I worked on a Service Management problem with my friend and colleague Wenming Ye from Microsoft. We looked at how easy it was to construct a Java Service Management API example. The criteria for us was fairly simple.

  • Extract a Certificate from a .publishsettings file
  • Use the certificate in a temporary call or context (session) and then delete when the call is finished
  • Extract the relevant subscription information from .publishsettings file too
  • Make a call to the WASM API to check whether a cloud service name is available

Simple right? Wrong!

Although I’ve been a Microsoft developer for all of my career I have written a few projects in Java in the past so am fairly familiar with Java Enterprise Edition. I even started a masters in Internet Technology (University of Greenwich) which revolved around Java 1.0 in 1996/97! In fact I’m applying my Java experience to Hadoop now which is quite refreshing!

I used to do a lot of Crypto work and was very familiar with the excellent Java provider model established in the JCE. As such I thought that the certificate management would be fairly trivial. It is but there are a a few gotchas. We’ll go over them now so that we can hit the problem and resolution before we hit the code.

The Sun Crypto provider has a problem with loading a PKCS#12 struct which contains the private key and associated certificate. In C# System.Cryptography is specifically built for this extraction and there is fairly easy and fluent way of importing the private key and certificate into the Personal store. Java has a keystore file which can act like the certificate store so in theory the PKCS#12/PFX (not the same exactly but for the purposes of this article they are) resident in the .publishsettings can be imported into the keystore.

In practice the Sun Provider doesn’t support unpassworded imports so this will always fail. If anybody has read my blog posts in the past you will know that I am a big fan of BouncyCastle (BC) and have used it in the past with Java (it’s original incarnation). Swapping the BC provider in place of the Sun one fixes this problem.

Let’s look at the code. To being we import the following:

import java.io.*;
import java.net.URL;

import javax.net.ssl.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

import java.security.*;

The BC import is necessary and the Xml namespaces used to parse the .publishsettings file.

We need to declare the following variables to hold details of the Service Management call and the keystore file details:

// holds the name of the store which will be used to build the output
private String outStore;
// holds the name of the publishSettingsFile
private String publishSettingsFile;
// The value of the subscription id that is being used
private String subscriptionId;
// the name of the cloud service to check for
private String name;

We’ll start by looking at the on-the-fly creation of the Java Keystore. Here we get a Base64 encoded certificate and after adding the BC provider and getting an instance of a PKCS#12 keystore we setup an empty store. When this is done we can decode the PKCS#12 structure into a byte input stream, add to the store (with an empty password) and write the store out, again with an empty password to a keystore file.

/* Used to create the PKCS#12 store - important to note that the store is created on the fly so is in fact passwordless - 
* the JSSE fails with masqueraded exceptions so the BC provider is used instead - since the PKCS#12 import structure does 
* not have a password it has to be done this way otherwise BC can be used to load the cert into a keystore in advance and 
* password*/
private KeyStore createKeyStorePKCS12(String base64Certificate) throws Exception	{
	Security.addProvider(new BouncyCastleProvider());
	KeyStore store = KeyStore.getInstance("PKCS12", BouncyCastleProvider.PROVIDER_NAME);
	store.load(null, null);

	// read in the value of the base 64 cert without a password (PBE can be applied afterwards if this is needed
	InputStream sslInputStream = new ByteArrayInputStream(Base64.decode(base64Certificate));
	store.load(sslInputStream, "".toCharArray());

	// we need to a create a physical keystore as well here
	OutputStream out = new FileOutputStream(getOutStore());
        store.store(out, "".toCharArray());
        out.close();
        return store;
}

Of course, in Java, you have to do more work to set the connection up in the first place. Remember the private key is used to sign messages. Your Windows Azure subscription has a copy of the certificate so can verify each request. This is done at the Transport level and TLS handles the loading of the client certificate so when you set up a connection on the fly you have to attach the keystore to the SSL connection as below.

/* Used to get an SSL factory from the keystore on the fly - this is then used in the
* request to the service management which will match the .publishsettings imported
* certificate */
private SSLSocketFactory getFactory(String base64Certificate) throws Exception	{			KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
	KeyStore keyStore = createKeyStorePKCS12(base64Certificate);

	// gets the TLS context so that it can use client certs attached to the
	SSLContext context = SSLContext.getInstance("TLS");
	keyManagerFactory.init(keyStore, "".toCharArray());
	context.init(keyManagerFactory.getKeyManagers(), null, null);

	return context.getSocketFactory();
}

The main method looks like this. It should be fairly familiarly to those of you that have been working with the WASM API for a while. we load and parse the XML, add the required headers to the request, send it and parse the response.

ServiceManager manager = new ServiceManager();
	try {
		manager.parseArgs(args);
		// Step 1: Read in the .publishsettings file 
		File file = new File(manager.getPublishSettingsFile());
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = dbf.newDocumentBuilder();
		Document doc = db.parse(file);
		doc.getDocumentElement().normalize();
		// Step 2: Get the PublishProfile 
		NodeList ndPublishProfile = doc.getElementsByTagName("PublishProfile");
		Element publishProfileElement = (Element) ndPublishProfile.item(0);
		// Step 3: Get the PublishProfile 
		String certificate = publishProfileElement.getAttribute("ManagementCertificate");
		System.out.println("Base 64 cert value: " + certificate);
		// Step 4: Load certificate into keystore 
		SSLSocketFactory factory = manager.getFactory(certificate);
		// Step 5: Make HTTP request - https://management.core.windows.net/[subscriptionid]/services/hostedservices/operations/isavailable/javacloudservicetest 
		URL url = new URL("https://management.core.windows.net/" + manager.getSubscriptionId() + "/services/hostedservices/operations/isavailable/" + manager.getName());
		System.out.println("Service Management request: " + url.toString());
		HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
		// Step 6: Add certificate to request 
		connection.setSSLSocketFactory(factory);
		// Step 7: Generate response 
		connection.setRequestMethod("GET");
		connection.setRequestProperty("x-ms-version", "2012-03-01");
		int responseCode = connection.getResponseCode();
		// response code should be a 200 OK - other likely code is a 403 forbidden if the certificate has not been added to the subscription for any reason 
		InputStream responseStream = null;
		if(responseCode == 200) {
			responseStream = connection.getInputStream();
		}  else  {
			responseStream = connection.getErrorStream();
		}
		BufferedReader buffer = new BufferedReader(new InputStreamReader(responseStream));
		// response will come back on a single line
		String inputLine = buffer.readLine();
		buffer.close();
		// get the availability flag
		boolean availability = manager.parseAvailablilityResponse(inputLine);
		System.out.println("The name " + manager.getName() + " is available: " + availability);
	}
	catch(Exception ex) {
		System.out.println(ex.getMessage());
	}
	finally	{
		manager.deleteOutStoreFile();
	}
}

For completeness, in case anybody wants to try this sampe out here is the rest.

/* <AvailabilityResponse xmlns="http://schemas.microsoft.com/windowsazure"
* xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
* <Result>true</Result>
* </AvailabilityResponse>
* Parses the value of the result from the returning XML*/
private boolean parseAvailablilityResponse(String response) throws ParserConfigurationException, SAXException, IOException {
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	DocumentBuilder db = dbf.newDocumentBuilder();

	// read this into an input stream first and then load into xml document
	@SuppressWarnings("deprecation")
	StringBufferInputStream stream = new StringBufferInputStream(response);
	Document doc = db.parse(stream);
	doc.getDocumentElement().normalize();
	// pull the value from the Result and get the text content
	NodeList nodeResult = doc.getElementsByTagName("Result");
	Element elementResult = (Element) nodeResult.item(0);
	// use the text value to return a boolean value
	return Boolean.parseBoolean(elementResult.getTextContent());
}

// Parses the string arguments into the class to set the details for the request
private void parseArgs(String args[]) throws Exception {
	String usage = "Usage: ServiceManager -ps [.publishsettings file] -store [out file store] -subscription [subscription id] -name [name]";
	if(args.length != 8)
		throw new Exception("Invalid number of arguments:\n" + usage);
	for(int i = 0; i < args.length; i++)	{
		switch(args[i])		{
			case "-store":
				setOutStore(args[i+1]);
				break;
			case "-ps":
				setPublishSettingsFile(args[i+1]);
				break;
			case "-subscription":
				setSubscriptionId(args[i+1]);
				break;
			case "-name":
				setName(args[i+1]);
				break;
			}
	}
	// make sure that all of the details are present before we begin the request
	if(getOutStore() == null || getPublishSettingsFile() == null || getSubscriptionId() == null || getName() == null)
		throw new Exception("Missing values\n" + usage);
}

// gets the name of the java keystore
public String getOutStore() {
	return outStore;
}

// sets the name of the java keystore
public void setOutStore(String outStore) {
	this.outStore = outStore;
}

// gets the name of the publishsettings file
public String getPublishSettingsFile() {
	return publishSettingsFile;
}

// sets the name of the java publishsettings file
public void setPublishSettingsFile(String publishSettingsFile) {
	this.publishSettingsFile = publishSettingsFile;
}

// get the value of the subscription id
public String getSubscriptionId() {
	return subscriptionId;
}

// sets the value of the subscription id
public void setSubscriptionId(String subscriptionId) {
	this.subscriptionId = subscriptionId;
}

// get the value of the subscription id
public String getName() {
	return name;
}

// sets the value of the subscription id
public void setName(String name) {
	this.name = name;
}

// deletes the outstore keystore when it has finished with it
private void deleteOutStoreFile()	{
	// the file will exist if we reach this point
	try	{
		java.io.File file = new java.io.File(getOutStore());
		file.delete();
	}
	catch(Exception ex){}
}

Last thing to say. Head to the BouncyCastle website to download the package and provider and in this implementation Wenming and I called the class ServiceManager.

Looking to the future and when I’ve got some time I may look at porting Fluent Management to Java using this technique. As it stands I feel this is a better technique than using the certificate store to manage the keys and underlying collection in that you don’t need elevated privileges to interact. A consequence which has proved a little difficult to work with when I’ve been using locked down clients in the workplace or Windows 8.

I’ve been working on both Windows Azure Active Directory recently and the new OPC package format with Fluent Management so expect some more blog posts shortly. Happy trails etc.

About these ads