Thursday, September 30, 2010

Double Trouble

You've heard it said: the cardinal rule of Java programming... "Never use a double to hold currency". Ok, well it might not be the cardinal rule, but its pretty important. But every once in a while you think to yourself, "aww... just this once. It won't hurt anything". Let me save you the headache and explain why this is a terrible idea. I'll also show you some alternatives for all your money problems (programmatically speaking, of course).

Everyone knows that we humans use the base 10 numbering system. It makes sense, ten fingers... base 10... viola. But your computer was unfortunately created with only two fingers. For those less familiar with the topic of bases in numbering systems, you can think of it as the 'roll-over' point when you're counting. For base 10 (decimal), counting rolls over after you get to nine. For base 2 (binary), counting rolls over after 1. For example...
00 (0)
01 (1)
10 (2)
11 (3) 

Now, I'm not going to get into the nitty gritty of how your computer stores numbers for each of the common Java data types (browse some wikipedia articles if you're interested). As a general rule, any whole number can be exactly represented using binary notation. Any fractional number is not guaranteed to have an exact representation in binary. Just as 1/3 cannot be precisely represented in decimal format, many fractional numbers also cannot be precisely represented in binary format. Here's a good test to run if you aren't convinced:
System.out.println(
  0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f
);
//Result: 0.8000001

System.out.println(
  0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d
);
//Result: 0.7999999999999999
You would expect adding 0.1 eight times would give you exacly 0.8, but the results can sometimes surprise you. You can imagine the havoc this could cause if you are attempting to perform calculations on money stored as Doubles or Floats.

Solution #1:
Only deal with cents. This means multiplying your decimal number by 100 and storing it as a Long. This is a safe operation on currency because, even though your initial decimal value is not exactly represented in binary format, it will be properly rounded by the JVM when converting it to a Long. However, be sure to do this before any other calculations are performed on it. If not, you could will introduce rounding errors.
double amount = 100.01d;
long amountInPennies = (long)(amount * 100);

Solution #2:
Use BigDecimal. This type can safely handle currency represented as dollars and cents because it essentially employs solution #1 for us behind the scenes. It either detects, or you can provide it, the precision that you require. The easiest (and safest) way to construct a BigDecimal is with the String constructor.

BigDecimal num = new BigDecimal("100.01");

It will detect a number with two decimal places (hundredths) and will internally multiply that number by 100, storing the result as a whole number. The BigDecimal class provides methods for all common arithmetic operations you may need to perform. This guarantees that you won't be plagued by the rounding errors and inaccuracies inherent in doubles and floats.

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.