How to programmatically set the SSLContext of a JAX-WS client?
Asked Answered
G

10

67

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?

Gilgai answered 12/6, 2012 at 16:48 Comment(2)
The bug report says a fix will be available in 2.1.1.Sublimity
@EJP That bug report is rather a feature request and does not mention how it should/will be done.Gilgai
G
60

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 () 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. ∎

Gilgai answered 14/6, 2012 at 11:50 Comment(8)
This looks only useful if your WSDL is accessible under http://, but what if it's also under https://? The client fails on the first step (getting WSDL).Strategic
We had the same situation and ended up having a copy of the wsdl's locally. Otherwise you have precisely this bootstrap problem.Gilgai
FYI: if your application is using JAXWS-RI then you should use a slightly different property: com.sun.xml.ws.transport.https.client.SSLSocketFactory. I specify both to satisfy different stacks in unit testing and actual application environment (don't ask).Legging
@mvreijn, man you saved my life.... i know there are constants that represent these literal strings... do you know what are they?Remedial
@RafaelLima I checked my source code documentation (it exists!) and I had documented a link to jax-ws.java.net/nonav/2.2.8/javadocs/rt/constant-values.html. Unfortunately the project has moved so the list of constants is in a different place so it seems.Legging
@Gilgai While providing the socket factory, how can we enforce TLS v1. 2?Nitrobenzene
FYI, to continue using the WSDL over SSL, I just did a manual fetch of WSDL XML document and saved it as a file before I did anything with the JAX-WS API. Then used the above answers to switch from a file URL to the correct URLInclining
@Gilgai Could you tell me how you did "dynamically changing the web service endpoint" ? I have exactly the same situtation, loading wsdl as a file, but have no idea how to managed to dynamically change ws url ?Hyperbola
L
24

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.

Lashanda answered 30/10, 2012 at 13:30 Comment(2)
What version of everything? It looks like different versions of wsimport generate different kinds of code... it's possible that they require different property-names as well.Weight
When you explicitly add jaxws-rt JARs to your application you need to use the property names that DON'T contain .internal.. If you use the JAXWS-RT included in the JDK you need to use the ones containing ".internal.". JAXWSProperties.SSL_SOCKET_FACTORY resolved to "com.sun.xml.ws.transport.https.client.SSLSocketFactory"Ectomy
C
23

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() );
Copier answered 17/9, 2012 at 17:32 Comment(5)
Where is webservicePort defined in the above code (it's used on last line of code after the (BindingProvider) cast?Etamine
@Radek, mind stating what is the type of webservicePort? It looks like to be javax.xml.ws.Service but not sure.Lunkhead
Guys, I posted this back in 2012. I honestly don't remember where I defined that port. Sorry that it's not explained but I don't have access to that code anymore to actually check and let you know. I might have taken it from an injected parameter and not had to define it myself, not sure. It should not be that hard to figure it out I suppose if you do some research on BindingProvider and what instantiates it. If I made it work back then, so can you. Good luck!Copier
@Etamine webservicePort is defined in wsimport generated class.Papaya
Thanks a lot :) This made my day!Nicholle
D
6

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
Duplicator answered 23/7, 2015 at 21:1 Comment(5)
Does keystore in getResourceAsStream(keystore) refer to the path of the certificate file?Schmo
In this case, it's the path within the class path. You can use a File instead of getClass().getResourceAsStream() if you need absolute file path.Duplicator
not great for me because I make other calls that I want to use the default ssl socket factory and this would stuff them upNephron
Works for me. Thanks for nice answer.Marelya
This worked perfectly for me as I my usecase required the context to be switched based off a parameter in the header. Thank youPolychrome
M
4

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!

Marmoset answered 20/11, 2018 at 21:20 Comment(0)
S
3

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>
Strategic answered 25/10, 2013 at 10:40 Comment(0)
C
1

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.

Cherise answered 14/4, 2014 at 6:58 Comment(1)
I use Wildfly 8 and it does not work for me either. (What do you mean with dynamic Dispatcher? - Please check my Posts: #37159321 and #37159321Denudate
A
1

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.

Aurlie answered 12/6, 2017 at 21:0 Comment(0)
G
1

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;
    }
Gearing answered 30/10, 2021 at 11:44 Comment(0)
B
0

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...

Bagnio answered 23/6, 2017 at 17:17 Comment(2)
Please quote the most relevant part of the link, in case the target site is unreachable or goes permanently offline.Reticle
This answer would set the trust store for the entire application. Relevant portion: -Djavax.net.ssl.trustStore=$JAVA_HOME/jre/lib/security/cacerts -Djavax.net.ssl.trustStorePassword=passwordMicrocyte

© 2022 - 2024 — McMap. All rights reserved.