66

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?

tshepang
  • 12,111
  • 21
  • 91
  • 136
maasg
  • 37,100
  • 11
  • 88
  • 115

10 Answers10

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

joergl
  • 2,850
  • 4
  • 36
  • 42
maasg
  • 37,100
  • 11
  • 88
  • 115
  • 2
    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). – Lukasz Frankowski Oct 25 '13 at 10:04
  • 3
    We had the same situation and ended up having a copy of the wsdl's locally. Otherwise you have precisely this bootstrap problem. – maasg Oct 25 '13 at 10:22
  • 6
    *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). – mvreijn Aug 15 '16 at 14:46
  • @mvreijn, man you saved my life.... i know there are constants that represent these literal strings... do you know what are they? – Rafael Lima Aug 01 '20 at 02:32
  • @RafaelLima I checked my source code documentation (it exists!) and I had documented a link to https://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. – mvreijn Aug 10 '20 at 21:27
  • @maasg While providing the socket factory, how can we enforce TLS v1. 2? – Crosk Cool Nov 23 '20 at 15:31
  • 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 URL – Hamy Mar 03 '21 at 22:30
  • @maasg 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 ? – Filip Kowalski Aug 19 '21 at 10:17
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() );
Community
  • 1
  • 1
Radek
  • 686
  • 6
  • 4
  • Where is webservicePort defined in the above code (it's used on last line of code after the (BindingProvider) cast? – John Mar 14 '18 at 00:44
  • @Radek, mind stating what is the type of webservicePort? It looks like to be javax.xml.ws.Service but not sure. –  Dec 15 '18 at 20:49
  • 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! – Radek Dec 19 '18 at 21:59
  • 1
    @John webservicePort is defined in wsimport generated class. – Maforast Apr 03 '19 at 20:01
  • Thanks a lot :) This made my day! – Praveesh P Sep 29 '20 at 15:16
23

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.

user254875486
  • 11,190
  • 7
  • 36
  • 65
Igors Sakels
  • 567
  • 5
  • 5
  • 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. – Christopher Schultz Sep 12 '13 at 19:18
  • 10
    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"` – Philip Helger Jan 28 '16 at 08:49
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
cidus
  • 61
  • 1
  • 4
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!

lunicon
  • 1,690
  • 1
  • 15
  • 26
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>
Lukasz Frankowski
  • 2,955
  • 1
  • 31
  • 32
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.

Davio
  • 4,609
  • 2
  • 31
  • 58
  • I use Wildfly 8 and it does not work for me either. (What do you mean with *dynamic Dispatcher*? - Please check my Posts: http://stackoverflow.com/questions/37158821/wildfly-how-to-use-jaxws-ri-instead-of-apache-cxf-webservice-client-only and http://stackoverflow.com/questions/37158821/wildfly-how-to-use-jaxws-ri-instead-of-apache-cxf-webservice-client-only – badera May 11 '16 at 12:11
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.

tschlegel
  • 21
  • 2
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;
    }
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
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...

  • 3
    Please quote the most relevant part of the link, in case the target site is unreachable or goes permanently offline. – Błażej Michalik Jun 23 '17 at 17:20
  • 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=password` – EpicVoyage Apr 24 '19 at 17:04