Monday, April 5, 2010

Weblogic and Wildcard SSL Certificates

I've recently been working on setting up an instance of Weblogic 10.3 for a new app I'm working on. This app makes several calls to a web service over SSL that is configured with a wildcard certificate. That means the certificate was issues to *.mydomain.com. This is a slick mechanism; it saves money and is easier to maintain all of your subdomains.



The Problem: Weblogic doesn't handle wildcard certs out of the box.
When weblogic initiates a connection over SSL, it makes a call to the HostnameVerifier. The default verifier simply ensures that the hostname you're connecting to and the name that the certificate was issued for match. So you can see the problem with wildcard certs in this example. The webservice is on ws.mydomain.com and the SSL certificate was issued to *.mydomain.com. If you google this problem, you will probably find lots of 'solutions' that suggest you disable hostname verification, but this leaves you vulnerable to man-in-the-middle attacks.



TheSolution: You might also find a few who would suggest you create your own HostnameVerifier. This is what we want, but I was unable to find any examples that actually show you how to do this! So here I will show you my implementation. Its not perfect, but I think it is certainly more than enough to get you started. DISCLAIMER: Most of the code is in a try/catch block. You'll notice that my code returns true on an error. Most likely, if your site has been compromised, the code will still run without exceptions and it should detect that the hostname and certificate don't match. If this your production system and you're super paranoid, just have the catch block return false.


package com.andrewthompson.weblogic;

import java.io.ByteArrayInputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.SSLSession;

import weblogic.security.SSL.HostnameVerifier;

/**
* Verify the hostname of outbound ssl connections. 
* 
* @author andrew.thompson
*
*/
public class WildcardHostnameVerifier implements HostnameVerifier 
{

public boolean verify(String hostname, SSLSession session)
{
try
{
Certificate cert = session.getPeerCertificates()[0];

byte [] encoded = cert.getEncoded();

CertificateFactory cf = CertificateFactory.getInstance("X.509");
ByteArrayInputStream bais = new ByteArrayInputStream(encoded);

X509Certificate xcert = (X509Certificate)cf.generateCertificate(bais);

String cn = getCanonicalName( xcert.getSubjectDN().getName() );

log.info("CN: " + cn);
log.info("HOSTNAME: " + hostname);

if(cn.equals(hostname))
return true;

Pattern validHostPattern = Pattern.compile("\\*\\.[^*]*\\.[^*]*");
Matcher validHostMatcher = validHostPattern.matcher(cn);

// Make sure the cert only has wildcard in subdomain.  We don't
//    want to confirm *.*.com now do we?  
if(validHostMatcher.matches())
{
String regexCn = cn.replaceAll("\\*", "(.)*");

log.info("REGEXCN: " + regexCn);

Pattern pattern = Pattern.compile(regexCn);
Matcher matcher = pattern.matcher(hostname);

if(matcher.matches())
{
log.info("Pattern MATCHES");
if(matcher.group().equals(hostname))
{
log.info("Group() matches hostname: " + matcher.group());
return true;
} else {
log.info("Group() doesn't match hostname: " + matcher.group());
return false;
}
} else {
log.info("Pattern DOESN'T MATCH");
return false;
}

}


} catch (Exception e ) {
e.printStackTrace();
return true;
}

return true;
}

/**
* Return just the canonical name from the distinguishedName 
* on the cert.
*  
* @param subjectDN
* @return
*/
private String getCanonicalName(String subjectDN)
{

Pattern pattern = Pattern.compile("CN=([-.*aA-zZ0-9]*)");
Matcher matcher = pattern.matcher(subjectDN);

if(matcher.find())
{
return matcher.group(1);
}

log.info("Couldn't find match for CN in subject");
return subjectDN;

}


private final static Logger log = Logger.getLogger(WildcardHostnameVerifier.class.getCanonicalName());

}


This is simply a matter of implementing the weblogic.security.SSL.HostnameVerifier interface. You will find this class in the weblogic.jar of your server/lib dir.
You'll notice that weblogic provides us with the SSLSession. From here, we can extract the java.security.cert.Certificate. We then use a CertificateFactory to transform this Certificate into an X509 certificate. That allows us to pull some useful information out, such an the Subject Distinguished Name. This is a string of key/value pairs; we are only interested in the Common Name or CN. So, we extract the CN with a regular expression pattern matcher. If the hostname matches the CN, we're done. If not, we check to see if the CN has a wildcard (*). If so, we use another regular expression matcher to see if the hostname matches the CN with any subdomain.



Now, you just need to go into the weblogic console, select the SSL tab on your server. Check 'Custom Hostname Verifier', enter the classname of your verifier, and make sure your class is on weblogic's classpath.