Thursday, June 19, 2014

Authentication with Client Certificate over HTTPS/SSL using Java – Handshake

Authentication with Client Certificate over HTTPS/SSL using Java – Handshake




To save somebody some time in the future, a step by step instruction is provided below:
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

Enabling SSL for AXIS2 service and client
We often encounter the satuation where requirement is to consume webservice exposed on https. In this article we will investigate how to consume webservice exposed over https using axis2. First lets see how to enable SSL for AXIS2 services:

Enabling SSL on server side for AXIS2 in tomcat:

You really don't need to do much enable SSL for services deplyed in AXIS2. Just follow how to enable SSL in tamcat.Add following in Server.xml of tamcat.
 

<Connector
 
port="8443" maxThreads="200"
scheme="https" secure="true" SSLEnabled="true"
keystoreFile="test.jks" keystorePass="test123"
clientAuth="false" sslProtocol="TLS"/>


Use axis2 1.5.3 in which axis2.xml has https transportReceiver enabled by default, listening on 8443 port so you don't need any configuration change in axis2.xml.
 
<transportReceiver name="https"
class="org.apache.axis2.transport.http.AxisServletListener">
<parameter name="port">8443</parameter>
</transportReceiver>

Prior to this version of axis2 was using org.apache.axis2.transport.nhttp.HttpCoreNIOSSLListener as transportReceive which was having issues and not generating https endpoint correctly.

SSL on client side:

Axis2 uses http commons to transfer SOAP message over http. Apache http common uses JSSE(java secure socket extension) library for SSL.
 
JSSE is integrated with JDK since version 1.4.
 

Ideally if we just provide end point URL starting with https, SSL connection will be started and we don’t need any additional configuration. Creation of secure connection will be taken care by JSSE. But then trust store and keystore used by the JSSE would be default keystores shipped with JDK.

In the practical/production scenarios user should have capability to choose his truststore/keystore.
 
user may decide to trust a self signed certificate and keep it his local truststore or different applications may use different keystore/truststore.
Above can be achieved by two ways:
Approach 1:
We can set truststore, password etc in system properties. This will be picked by JSSE in SSL handshake.
Ex.
System.setProperty("javax.net.ssl.trustStore","Your truststore path");
System.setProperty("javax.net.ssl.trustStorePassword","your trust store password");
This approach will not be appropriate for tooling since it sets keystore on JVM level. We should have flexibility where we could attach different keystore/truststore for different axis2 clint running in same JVM.

Approach 2:
Apache commons provide facility which allows us to customize the SSL socket factory responsible for creation of secure socket. By customization I mean ability to use user truststore/keystore in SSL handshake. To achieve it we need to extend SecureProtocolSocketFactory interface. In our custom socket factory implementation user refer its Keystore/Truststore against default keystores.
Apache commons provide a reference implementation class named AuthSSLProtocolSocketFactory for this purpose.

This class takes Truststore/Keystore as argument to constructor which will be referred later while initiating SSLContext. SSLContext is used to create SSL Socket Factory.
In your axis2 client code you need to add following:
Protocol authhttps = new Protocol ("https", new AuthSSLProtocolSocketFactory (new url("keystore URL"), "pwd", newURL("truststore URL"), "pwd"), 443);
Protocol.registerProtocol("https", authhttps);


package bo.socket;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.util.Hashtable;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import org.apache.axis.components.net.JSSESocketFactory;
import org.apache.axis.components.net.SecureSocketFactory;

public class MyCustomSSLSocketFactory extends JSSESocketFactory implements SecureSocketFactory
 

{ public MyCustomSSLSocketFactory(Hashtable attributes) {
super(attributes);
}

protected void initFactory() throws IOException {

try {
SSLContext context = getContext();
sslFactory = context.getSocketFactory();
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
}
System.out.print(e.getMessage());
throw new IOException(e.getMessage());
}
}

protected SSLContext getContext() throws Exception
 
{
 
try
{
String keystore_type = KeyStore.getDefaultType(); // "JKS"

KeyStore keyStore = KeyStore.getInstance(keystore_type);
KeyStore trustStore = KeyStore.getInstance(keystore_type);

char[] keystore_password = "DemoIdentityKeyStorePassPhrase".toCharArray();
keyStore.load(new FileInputStream("C:\\bea103\\wlserver_10.3\\server\\lib\\DemoIdentity.jks"), keystore_password);

char[] trusstore_password = "DemoTrustKeyStorePassPhrase".toCharArray();
trustStore.load(new FileInputStream("C:\\bea103\\wlserver_10.3\\server\\lib\\DemoTrust.jks"), trusstore_password);

String algorithmTrust = TrustManagerFactory.getDefaultAlgorithm(); // PKIX

TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithmTrust);
tmf.init(trustStore);

String algorithmKey = KeyManagerFactory.getDefaultAlgorithm(); // "SunX509"
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithmKey);

char[] key_password = "DemoIdentityPassPhrase".toCharArray();

kmf.init(keyStore, key_password);

SSLContext sslctx = SSLContext.getInstance("SSL");

sslctx.init(kmf.getKeyManagers(),tmf.getTrustManagers(),null);

return sslctx;
}
catch (Exception e)
{ e.printStackTrace();
throw new Exception("Error creating context for SSLSocket.", e);
}
}
}

And in the main call of a WS from Client I set the new SSLSocketFactory Class:
AxisProperties.setProperty("axis.socketSecureFactory","bo.socket.MyCustomSSLSocketFactory"); 


openssl

generate a new private key and matching Certificate Signing Request (eg to send to a commercial CA)
openssl req -out MYCSR.csr -pubkey -new -keyout MYKEY.key
add -nodes to create an unencrypted private key
add 
-config <openssl.cnf> if your config file has not been set in the environment
decrypt private key
openssl rsa -in MYKEY.key >> MYKEY-NOCRYPT.key
generate a certificate siging request for an existing private key
openssl req -out MYCSR.csr -key MYKEY.key -new
generate a certificate signing request based on an existing x509 certificate
openssl x509 -x509toreq -in MYCRT.crt -out MYCSR.csr -signkey MYKEY.key
create self-signed certificate (can be used to sign other certificates)
openssl req -x509 -new -out MYCERT.crt -keyout MYKEY.key -days 365
sign a Certificate Signing Request
openssl x509 -req -in MYCSR.csr -CA MY-CA-CERT.crt -CAkey MY-CA-KEY.key -CAcreateserial -out MYCERT.crt -days 365
-days has to be less than the validity of the CA certificate




convert DER (.crt .cer .der) to PEM
openssl x509 -inform der -in MYCERT.cer -out MYCERT.pem
convert PEM to DER
openssl x509 -outform der -in MYCERT.pem -out MYCERT.der
convert PKCS#12 (.pfx .p12) to PEM containing both private key and certificates
openssl pkcs12 -in KEYSTORE.pfx -out KEYSTORE.pem -nodes
add -nocerts for private key only; add -nokeys for certificates only
convert (add) a seperate key and certificate to a new keystore of type PKCS#12
openssl pkcs12 -export -in MYCERT.crt -inkey MYKEY.key -out KEYSTORE.p12 -name "tomcat"
convert (add) a seperate key and certificate to a new keystore of type PKCS#12 for use with a server that should send the chain too (eg Tomcat)
openssl pkcs12 -export -in MYCERT.crt -inkey MYKEY.key -out KEYSTORE.p12 -name "tomcat" -CAfile MY-CA-CERT.crt -caname myCA -chain
you can repeat the combination of "-CAfile" and "-caname" for each intermediate certificate




check a private key
openssl rsa -in MYKEY.key -check
add -noout to not disclose the key
check a Certificate Signing Request
openssl req -text -noout -verify -in MYCSR.csr
check a certificate
openssl x509 -in MYCERT.crt -text -noout
check a PKCS#12 keystore
openssl pkcs12 -info -in KEYSTORE.p12
check a trust chain of a certificate
openssl verify -CAfile MYCHAINFILE.pem -verbose MYCERT.crt
trust chain is in directory (hash format): replace -CAfile with -CApath /path/to/CAchainDir/
to check for server usage: 
-purpose sslserver
to check for client usage: 
-purpose sslient
check if public key matches the private key
openssl rsa -in MYKEY.key -modulus -noout | openssl md5; /
openssl x509 -in MYCERT.crt -modulus -noout | openssl md5
This should return the same two md5-hashes




debug an SSL connection [server doesn't require certificate authentication]
openssl s_client -connect idp.example.be:443
debug an SSL connection with mutual certificate authentication
openssl s_client -connect idp.example.be:8443 -CAfile MY-CA-CERT.crt -cert MYCERT.crt -key MYKEY.key
trust chain is in directory (hash format): replace -CAfile with -CApath /path/to/CAchainDir/
send the starttls command (smtp or pop3 style): 
-starttls smtp or -starttls pop3

keytool

keytool does not support management of private keys inside a keystore. You need to use another tool for that. If you are using the JKS format, that means you need another java-based tool. extkeytool from the Shibboleth distribution can do this. 
Create an empty keystore
keytool -genkey -alias foo -keystore truststore.jks
keytool -delete -alias foo -keystore truststore.jks
Generate a private key and an initial certificate as a JKS keystore
keytool -genkey -keyalg RSA -alias "selfsigned" -keystore KEYSTORE.jks -storepass "secret" -validity 360
you can also pass the data for the DN of the certificate as command-line parameters: -dname "CN=${pki-cn}, OU=${pki-ou}, O=${pki-o}, L=${pki-l}, S=${pki-s}, C=${pki-c}"
Generate a secret key that can be used for symmetric encryption. For this to work, you need to make use of a JCEKS keystore.
keytool -genseckey -alias "secret_key" -keystore KEYSTORE.jks -storepass "secret" -storetype "JCEKS"
Generate a Certificate Signing Request for a key in a JKS keystore
keytool -certreq -v -alias "selfsigned" -keystore KEYSTORE.jks -storepass "secret" -file MYCSR.csr
Import a (signed) certificate into a JKS keystore
keytool -import -keystore KEYSTORE.jks -storepass "secret" -file MYCERT.crt
add a public certificate to a JKS keystore, eg the JVM truststore
keytool -import -trustcacerts -alias "sensible-name-for-ca" -file CAcert.crt -keystore MYSTORE.jks
If the JVM truststore contains your certificate or the certificate of the root CA that signed your certificate, then the JVM will trust and thus might accept your certificate. The default truststore already contains the root certificates of most commonly used sommercial CA's. Use this command to add another certificate for trust:
keytool -import -trustcacerts -alias "sensible-name-for-ca" -file CAcert.crt -keystore $JAVA_HOME/lib/security/cacerts
the default password of the Java truststore is "changeit".
if $JAVA_HOME is set to the root of the JDK, then the truststore is it $JAVA_HOME/jre/lib/security/cacerts
keytool does NOT support adding trust certificates to a PKCS12 keystore (which is very unfortunate but probably a good move to promote JKS)
delete a public certificate from a JAVA keystore (JKS; eg JVM truststore)
keytool -delete -alias "sensible-name-for-ca" -keystore $JAVA_HOME/lib/security/cacerts
the default password of the Java truststore is "changeit".
if $JAVA_HOME is set to the root of the JDK, then the truststore is it $JAVA_HOME/jre/lib/security/cacerts
List the certificates inside a keystore
keytool -list -v -keystore KEYSTORE.jks
-storetype pkcs12 can be used
Get information about a stand-alone certificate
keytool -printcert -v -file MYCERT.crt
Convert a JKS file to PKCS12 format (Java 1.6.x and above)
keytool -importkeystore -srckeystore KEYSTORE.jks -destkeystore KEYSTORE.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass mysecret -deststorepass mysecret -srcalias myalias -destalias myalias -srckeypass mykeypass -destkeypass mykeypass -noprompt




certutil




Add a PKCS12 to a windows certificate store
certutil -p secret -importpfx KEYSTORE.p12


Important Exceptions



1.) Unconnected sockets not implemented
    2.) SSL Handshake failure error

3.) org.apache.axis2.AxisFault: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

2 comments:

  1. My Self Harivigneshwaran I am trying 2 way ssl rest api i had .p12 file and implement in my coding i have 401 unauthorized error happen i am totally confused if any other file will be need kindly share valuable feed back

    ReplyDelete