The Problem
I had the problem to access a web service out of a Glassfish Application Server (3.1.1). Basically this is not a problem because Glassfish (and Metro – the JAX-WS implementation in Glassfish) can handle web services quite well. But in my case there where some problems accessing the web service. My problems were:
- The web service has to be accessed via SSL.
- The certificate should not be stored in a truststore.
- The certificate has not the hostname as the CN.
The first problem is not really a problem but with the second problem together with the first one can be really tough to solve. So lets solve this problems ;-).
Connecting to a SSL Secured Web Service
Connecting a SSL secured web service with Metro is pretty straight forward. The problems begin when the target server has no certificate from a recognized CA. By default Glassfish has a set of predefined CA root certificates. If the server has a certificate that is signed by one of this CAs then you are already the half way done. If the CN of the certificate is the hostname of the target host then you web service works. So this is simple…
But real development problems are seldom so easy to solve…
Adding a CA Root Certificate or a Certificate to the Truststore
When the server has no certificate that is signed by a known CA them you have to add the CA‘s root certificate or the certificate to your truststore. Please be sure to add the certificate or the CA root certificate only if you really trust the issuer. To add a certificate you can use the following command at the command line:
keytool -importcert -file -keystore cacerts.jks
The file cacerts.jks
is in the config directory of the Glassfish domain.
If the CN of the certificate is the hostname of the target server then your mission is accomplished. Your setup should now work.
Whenever possible use a certificate with the hostname as the CN. Everything else is difficult to setup so that the security of SSL is not tampered (see also this blog entry by Bruce Schneier).
Using Another Way to Retrieve the Certificate
When the certificate or the CA‘s root certificate cannot be added to the truststore (e.g. because the certificate has to be retrieved by another way like LDAP or from a database) then you have to find another way.
I had to retrieve the certificate via another way (and also I had to use a way to easily change the implementation to retrieve the certificate because it was LDAP and also from a database but this is another story). The first problem is creating the web service. By default you use the following way to get the web service:
WebService service = new WebService( new URL("https://example.com/?wsdl"));
This does not work because the WSDL cannot be retrieved because the certificate is not known.
To solve this problem you have to create the web service without naming the WSDL. This is simply done by using the following constructor:
WebService service = new WebService( getClass().getResource("/META-INF/wsdl/WebService.wsdl"));
I use the getResource()
to load the local WSDL out of the classpath. If necessary you have to adjust this path. The only downside with this approach is that you need the WSDL in your project.
The constructor without parameters can fail because then the WSDL used during the generation of the classes will be used which can be an absolute path in the file system which will fail if the path is not available in your runtime environment.
The next step is now to get the port from the service:
WebPort port = service.getWebPort();
Until this point there is nothing special with my solution.
The next step is now specific to Metro. Because I have to use Metro I’ve not searched for any solution with another web service framework.
You have to cast the port to the Metro specific BindingProvider
:
WSBindindProvider bp = (WSBindingProvider)port;
Now you can set the address of the web service (not the address of the WSDL):
bp.setAddress("https://example.com/");
Now we can create the SSLContext
to be used by Metro. For simplicity I assume that the certificate is in the file /META-INF/server.cer
in the classpath:
Map<String , Object> ctx = bp.getRequestContext(); KeyStore sslTrustStore = KeyStore.getInstance("JKS"); sslTrustStore.load(null, null); CertificateFactory cf = CertificateFactory.getInstance( "X.509"); int i = 0; for (Certificate cert: cf.generateCertificates( getClass().getResourceAsStream("/META-INF/server.cer"))) { sslTrustStore.setCertificateEntry(new StringBuilder( "Key").append(i++).toString(), cert); } KeyMananagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); kmf.init(null, new char[0]); TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(sslTrustStore); SSLContext sslCtx = SSLContext.getInstance("TLS"); sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); ctx.put(JAXWSProperties.SSL_SOCKET_FACTORY, sslCtx.getSocketFactory());
This sets up the SSLContext
with the certificate of the target host and sets it for the outgoing connection. Now it should be possible to call the external web service (as long as the CN of the certificate matches the hostname of the target server.
Checking for the Hostname in a Custom Way
Because in my scenario the target hostname is not the CN of the certificate the connection fails with an Exception
. So I also have to change the hostname validation.
Please only use code like this if you really know what you’re doing. If you mess up with this piece of code all SSL could be switched off, leaving you effectively unsecured without any encryption (see also this blog entry by Bruce Schneier).
In my case the hostname and the certificate are connected by a database entry (normally the certificate is bound to the hostname via the CN). Therefore I can check that the hostname of the web service I connect to is the same as saved in the database with the certificate. It is really important that you have a reliable connection between the hostname and the certificate:
HostnameVerifier hostnameVerifier = new HostnameVerifier() { @Override boolean verify(String hostname, SSLSession session) { return hostname.equals(databaseHostname); } }; ctx.put(JAXWSProperties.HOSTNAME_VERIFIER, hostnameVerifier);
Now I can connect to the configured host with the configured certificate. Important is that the connection between both can be trusted else you will be busted.
Now you know how to connect to a SSL secured web service even if you do not add the certificate to your truststore or have a certificate of a “trusted” CA.
It is the perfect solution for my problem – tested and running at 100% in Wildfly 9.
Thank you!
If the CN of the certificate is the hostname of the target host then you web service works. So this is simple
As long as the hostname is in the CN of the certificate there is no problem.
But as I mentioned if the hostname is not in the CN then you are not able to access the host, because the hostname is checked against the name in the CN. My solution has the advantage that you can check the CN against another value that you get from another source…