How to programmatically set the SSLContext of a JAX-WS client?
JavaSslCertificateJax WsJava Problem Overview
I'm working on a server in a distributed application that has browser clients and also participates in server-to-server communication with a 3rd party. My server has a CA-signed certificate to let my clients connect using TLS (SSL) communication using HTTP/S and XMPP(secure). That's all working fine.
Now I need to securely connect to a 3rd party server using JAX-WS over HTTPS/SSL. In this communication, my server acts as client in the JAX-WS interation and I've a client certificate signed by the 3rd party.
I tried adding a new keystore through the standard system configuration (-Djavax.net.ssl.keyStore=xyz
) but my other components are clearly affected by this. Although my other components are using dedicated parameters for their SSL configuration (my.xmpp.keystore=xxx, my.xmpp.truststore=xxy, ...
), it seems that they end up using the global SSLContext
. (The configuration namespace my.xmpp.
seemed to indicate separation, but it's not the case)
I also tried adding my client certificate into my original keystore, but -again- my other components don't seem to like it either.
I think that my only option left is to programmatically hook into the JAX-WS HTTPS configuration to setup the keystore and truststore for the client JAX-WS interaction.
Any ideas/pointers on how to do this? All information I find either uses the javax.net.ssl.keyStore
method or is setting the global SSLContext
that -I guess- will end up in the same confilc. The closest I got to something helpful was this old bug report that requests the feature I need: Add support for passing an SSLContext to the JAX-WS client runtime
Any takes?
Java Solutions
Solution 1 - Java
This one was a hard nut to crack, so for the record:
To solve this, it required a custom KeyManager
and a SSLSocketFactory
that uses this custom KeyManager
to access the separated KeyStore
.
I found the base code for this KeyStore
and SSLFactory
on this excellent blog entry:
how-to-dynamically-select-a-certificate-alias-when-invoking-web-services
Then, the specialized SSLSocketFactory
needs to be inserted into the WebService context:
service = getWebServicePort(getWSDLLocation());
BindingProvider bindingProvider = (BindingProvider) service;
bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory", getCustomSocketFactory());
Where the getCustomSocketFactory()
returns a SSLSocketFactory
created using the method mentioned above. This would only work for JAX-WS RI from the Sun-Oracle impl built into the JDK, given that the string indicating the SSLSocketFactory
property is proprietary for this implementation.
At this stage, the JAX-WS service communication is secured through SSL, but if you are loading the WSDL from the same secure server (HTTPS://server.com/service?wsdl) then you'll have a bootstrap problem, as the HTTPS request to gather the WSDL will not be using the same credentials than the Web Service. I worked around this problem by making the WSDL locally available (file:///...) and dynamically changing the web service endpoint: (a good discussion on why this is needed can be found in this forum)
bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, webServiceLocation);
Now the WebService gets bootstrapped and is able to communicate through SSL with the server counterpart using a named (alias) Client-Certificate and mutual authentication. ∎
Solution 2 - Java
This is how I solved it based on this post with some minor tweaks. This solution does not require creation of any additional classes.
SSLContext sc = SSLContext.getInstance("SSLv3");
KeyManagerFactory kmf =
KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() );
KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );
ks.load(new FileInputStream( certPath ), certPasswd.toCharArray() );
kmf.init( ks, certPasswd.toCharArray() );
sc.init( kmf.getKeyManagers(), null, null );
((BindingProvider) webservicePort).getRequestContext()
.put(
"com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory",
sc.getSocketFactory() );
Solution 3 - Java
I tried the following and it didn't work on my environment:
bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory", getCustomSocketFactory());
But different property worked like a charm:
bindingProvider.getRequestContext().put(JAXWSProperties.SSL_SOCKET_FACTORY, getCustomSocketFactory());
The rest of the code was taken from the first reply.
Solution 4 - Java
By combining Radek and l0co's answers you can access the WSDL behind https:
SSLContext sc = SSLContext.getInstance("TLS");
KeyManagerFactory kmf = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(getClass().getResourceAsStream(keystore),
password.toCharArray());
kmf.init(ks, password.toCharArray());
sc.init(kmf.getKeyManagers(), null, null);
HttpsURLConnection
.setDefaultSSLSocketFactory(sc.getSocketFactory());
yourService = new YourService(url); //Handshake should succeed
Solution 5 - Java
You can move your proxy authentication and ssl staff to soap handler
port = new SomeService().getServicePort();
Binding binding = ((BindingProvider) port).getBinding();
binding.setHandlerChain(Collections.<Handler>singletonList(new ProxyHandler()));
This is my example, do all network ops
class ProxyHandler implements SOAPHandler<SOAPMessageContext> {
static class TrustAllHost implements HostnameVerifier {
public boolean verify(String urlHostName, SSLSession session) {
return true;
}
}
static class TrustAllCert implements X509TrustManager {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
}
private SSLSocketFactory socketFactory;
public SSLSocketFactory getSocketFactory() throws Exception {
// just an example
if (socketFactory == null) {
SSLContext sc = SSLContext.getInstance("SSL");
TrustManager[] trustAllCerts = new TrustManager[] { new TrustAllCert() };
sc.init(null, trustAllCerts, new java.security.SecureRandom());
socketFactory = sc.getSocketFactory();
}
return socketFactory;
}
@Override public boolean handleMessage(SOAPMessageContext msgCtx) {
if (!Boolean.TRUE.equals(msgCtx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY)))
return true;
HttpURLConnection http = null;
try {
SOAPMessage outMessage = msgCtx.getMessage();
outMessage.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, "UTF-8");
// outMessage.setProperty(SOAPMessage.WRITE_XML_DECLARATION, true); // Not working. WTF?
ByteArrayOutputStream message = new ByteArrayOutputStream(2048);
message.write("<?xml version='1.0' encoding='UTF-8'?>".getBytes("UTF-8"));
outMessage.writeTo(message);
String endpoint = (String) msgCtx.get(BindingProvider.ENDPOINT_ADDRESS_PROPERTY);
URL service = new URL(endpoint);
Proxy proxy = Proxy.NO_PROXY;
//Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("{proxy.url}", {proxy.port}));
http = (HttpURLConnection) service.openConnection(proxy);
http.setReadTimeout(60000); // set your timeout
http.setConnectTimeout(5000);
http.setUseCaches(false);
http.setDoInput(true);
http.setDoOutput(true);
http.setRequestMethod("POST");
http.setInstanceFollowRedirects(false);
if (http instanceof HttpsURLConnection) {
HttpsURLConnection https = (HttpsURLConnection) http;
https.setHostnameVerifier(new TrustAllHost());
https.setSSLSocketFactory(getSocketFactory());
}
http.setRequestProperty("Content-Type", "application/soap+xml; charset=utf-8");
http.setRequestProperty("Content-Length", Integer.toString(message.size()));
http.setRequestProperty("SOAPAction", "");
http.setRequestProperty("Host", service.getHost());
//http.setRequestProperty("Proxy-Authorization", "Basic {proxy_auth}");
InputStream in = null;
OutputStream out = null;
try {
out = http.getOutputStream();
message.writeTo(out);
} finally {
if (out != null) {
out.flush();
out.close();
}
}
int responseCode = http.getResponseCode();
MimeHeaders responseHeaders = new MimeHeaders();
message.reset();
try {
in = http.getInputStream();
IOUtils.copy(in, message);
} catch (final IOException e) {
try {
in = http.getErrorStream();
IOUtils.copy(in, message);
} catch (IOException e1) {
throw new RuntimeException("Unable to read error body", e);
}
} finally {
if (in != null)
in.close();
}
for (Map.Entry<String, List<String>> header : http.getHeaderFields().entrySet()) {
String name = header.getKey();
if (name != null)
for (String value : header.getValue())
responseHeaders.addHeader(name, value);
}
SOAPMessage inMessage = MessageFactory.newInstance()
.createMessage(responseHeaders, new ByteArrayInputStream(message.toByteArray()));
if (inMessage == null)
throw new RuntimeException("Unable to read server response code " + responseCode);
msgCtx.setMessage(inMessage);
return false;
} catch (Exception e) {
throw new RuntimeException("Proxy error", e);
} finally {
if (http != null)
http.disconnect();
}
}
@Override public boolean handleFault(SOAPMessageContext context) {
return false;
}
@Override public void close(MessageContext context) {
}
@Override public Set<QName> getHeaders() {
return Collections.emptySet();
}
}
It use UrlConnection, you can use any library you want in handler. Have fun!
Solution 6 - Java
The above is fine (as I said in comment) unless your WSDL is accessible with https:// too.
Here is my workaround for this:
Set you SSLSocketFactory as default:
HttpsURLConnection.setDefaultSSLSocketFactory(...);
For Apache CXF which I use you need also add these lines to your config:
<http-conf:conduit name="*.http-conduit">
<http-conf:tlsClientParameters useHttpsURLConnectionDefaultSslSocketFactory="true" />
<http-conf:conduit>
Solution 7 - Java
For those trying and still not getting it to work, this did it for me with Wildfly 8, using the dynamic Dispatcher
:
bindingProvider.getRequestContext().put("com.sun.xml.ws.transport.https.client.SSLSocketFactory", yourSslSocketFactory);
Note that the internal
part from the Property key is gone here.
Solution 8 - Java
I had problems trusting a self signed certificate when setting up the trust manager. I used the SSLContexts builder of the apache httpclient to create a custom SSLSocketFactory
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStoreFile, "keystorePassword.toCharArray(), keyPassword.toCharArray())
.loadTrustMaterial(trustStoreFile, "password".toCharArray(), new TrustSelfSignedStrategy())
.build();
SSLSocketFactory customSslFactory = sslcontext.getSocketFactory()
bindingProvider.getRequestContext().put(JAXWSProperties.SSL_SOCKET_FACTORY, customSslFactory);
and passing in the new TrustSelfSignedStrategy()
as an argument in the loadTrustMaterial
method.
Solution 9 - Java
we faced this problem, due to a keystore clash between system integrations, so we used the following code.
private PerSecurityWS prepareConnectionPort() {
final String HOST_BUNDLE_SYMBOLIC_NAME = "wpp.ibm.dailyexchangerates";
final String PATH_TO_SLL = "ssl/<your p.12 certificate>";
final File ksFile = getFile(HOST_BUNDLE_SYMBOLIC_NAME, PATH_TO_SLL);
final String serverURI = "you url";
final KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(new FileInputStream(ksFile.getAbsolutePath()), keyStorePassword.toCharArray());
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keyStorePassword.toCharArray());
final HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
@Override
public boolean verify(final String hostname, final SSLSession session) {
return false;
}
};
final SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), null, null);
final SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();
final PerSecurityWS port = new PerSecurityWS_Service().getPerSecurityWSPort();
final BindingProvider bindingProvider = (BindingProvider) port;
bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory",sslSocketFactory);
bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serverURI);
bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.hostname.verifier",DO_NOT_VERIFY);
return port;
}
Solution 10 - Java
I tried the steps here:
http://jyotirbhandari.blogspot.com/2011/09/java-error-invalidalgorithmparameterexc.html
And, that fixed the issue. I made some minor tweaks - I set the two parameters using System.getProperty...