During the implementation, I got tripped more than a couple of times over error messages like:
403.7 Forbidden: Client Certificate Required.To save somebody some time in the future, a step by step instruction is provided below:
javax.net.ssl.SSLException: HelloRequest followed by an unexpected handshake message
- I assume you have a valid certificate or a chain of certificates, whose root is acceptable by the server. The valid certificate contains its private key. Run the following command to verify:
keytool -list -v -keystore "your certificate file"
...
Entry type: PrivateKeyEntry - Import your certificate and intermediate certificates into a browser like IE or Firefox and test out the https URL. This step will validate the certificates and save you a lot of troubles down the road. Java version of the SSL implementation is not as simple/mature as the browsers'. Please make sure all the certificates have not expired.
- Backup your keystore located at /your_home_directory/.keystore by default and the truststore located at somewhere similar to \Java\jre6\lib\security\cacerts
- Use not-yet-commons-ssl utility to import your certificates into the Java keystore format. Sample command is:
java -cp not-yet-commons-ssl-0.3.9.jar org.apache.commons.ssl.KeyStoreBuilder
- Customize the following java code, replace the static final Strings to fit in your needs. Note that this implementation forcefully use a specific alias to present the corresponding certificate/certificate chain to the server. Somehow the default KeyManager simply disqualifies my certificate to be presented to the server.
public class Main {
private static final Logger logger = Logger.getLogger(Main.class.getName());
private static final String LINE_BREAKER = System.getProperty("line.separator");
private static final String CERTIFACATE_FILE = "your keystore location";
private static final String CERTIFACATE_PASS = "changeit";
private static final String CERTIFACATE_ALIAS = "your alias";
private static final String TARGET_URL = "https://xyz.com";
public static void main(String[] args) {
String targetURL = TARGET_URL;
URL url;
HttpsURLConnection connection = null;
BufferedReader bufferedReader = null;
InputStream is = null;
try {
//Create connection
url = new URL(targetURL);
//Uncomment this in case server demands some unsafe operations
//System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true");
connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Content-Language", "en-US");
SSLSocketFactory sslSocketFactory = getFactory(new File(CERTIFACATE_FILE), CERTIFACATE_PASS, CERTIFACATE_ALIAS);
connection.setSSLSocketFactory(sslSocketFactory);
//Process response
is = connection.getInputStream();
bufferedReader = new BufferedReader(new InputStreamReader(is));
String line;
StringBuffer lines = new StringBuffer();
while ((line = bufferedReader.readLine()) != null) {
lines.append(line).append(LINE_BREAKER);
}
logger.info("response from " + targetURL + ":" + LINE_BREAKER + lines);
} catch (Exception e) {
...
}
}
private static SSLSocketFactory getFactory(File pKeyFile, String pKeyPassword, String certAlias) throws Exception {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
KeyStore keyStore = KeyStore.getInstance("JKS");
InputStream keyInput = new FileInputStream(pKeyFile);
keyStore.load(keyInput, pKeyPassword.toCharArray());
keyInput.close();
keyManagerFactory.init(keyStore, pKeyPassword.toCharArray());
//Replace the original KeyManagers with the AliasForcingKeyManager
KeyManager[] kms = keyManagerFactory.getKeyManagers();
for (int i = 0; i < kms.length; i++) {
if (kms[i] instanceof X509KeyManager) {
kms[i] = new AliasForcingKeyManager((X509KeyManager) kms[i], certAlias);
}
}
SSLContext context = SSLContext.getInstance("TLS");
context.init(kms, null, null);
return context.getSocketFactory();
}
/*
* This wrapper class overwrites the default behavior of a X509KeyManager and
* always render a specific certificate whose alias matches that provided in the constructor
*/
private static class AliasForcingKeyManager implements X509KeyManager {
X509KeyManager baseKM = null;
String alias = null;
public AliasForcingKeyManager(X509KeyManager keyManager, String alias) {
baseKM = keyManager;
this.alias = alias;
}
/*
* Always render the specific alias provided in the constructor
*/
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return alias;
}
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return baseKM.chooseServerAlias(keyType, issuers, socket);
}
public X509Certificate[] getCertificateChain(String alias) {
return baseKM.getCertificateChain(alias);
}
public String[] getClientAliases(String keyType, Principal[] issuers) {
return baseKM.getClientAliases(keyType, issuers);
}
public PrivateKey getPrivateKey(String alias) {
return baseKM.getPrivateKey(alias);
}
public String[] getServerAliases(String keyType, Principal[] issuers) {
return baseKM.getServerAliases(keyType, issuers);
}
}
} - Try to set
-Dsun.security.ssl.allowUnsafeRenegotiation=true
if you get the error message like:javax.net.ssl.SSLException: HelloRequest followed by an unexpected handshake message
- If anything goes wrong, turn on -Djavax.net.debug=all to debug. Verify the keystore and truststore locations. Verify the presence of the certificates. Here is a sample log file with successful connection: http://java.sun.com/javase/6/docs/technotes/guides/security/jsse/ReadDebug.html