Thứ Ba, 6 tháng 6, 2017

Send SOAP request over HTTPS without valid certificates (dangerous in production)

Post summary: How to send SOAP request over HTTPS in Java without generating and installing certificates. NB: This MUST not be used for production code!

SOAP (Simple Object Access Protocol) is a protocol used in web services. It allows exchange of XML data over HTTP or HTTPS.

Send SOAP over HTTP

Sending SOAP message over HTTP is Java is as simple as:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SOAPMessage sendSoapRequest(String endpointUrl, SOAPMessage request) {
    try {
        // Send HTTP SOAP request and get response
        SOAPConnection soapConnection
                = SOAPConnectionFactory.newInstance().createConnection();
        SOAPMessage response = soapConnection.call(request, endpointUrl);
        // Close connection
        soapConnection.close();
        return response;
    } catch (SOAPException ex) {
        // Do Something
    }
    return null;
}

HTTPS

HTTPS is a HTTP over a security layer (SSL/TLS). This is the essence of secure internet communication. For valid HTTPS connection server needs a valid certificate signed by certification authority. Establishing a HTTPS connection between client and server is by a procedure called SSL handshake in which client validates server certificate and both set session key which they use to encrypt messages. This level of security makes code above to fail if executed against HTTPS host:
javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative DNS name matching localhost found.
This error appears because server is running on localhost and there is no valid certificate for localhost. Proper way for handling this is to generate a valid or test SSL certificate with IP or hostname of the machine running the server and install it on it.

Trust all hosts

Generating and installing SSL certificates for test servers is a good idea but is not worth the effort. So in order to overcome this a HTTPS connection should be opened and it should be instructed to trust any host name. First is to add dummy implementation of HostnameVerifier interface trusting all hosts:
1
2
3
4
5
6
7
8
/**
 * Dummy class implementing HostnameVerifier to trust all host names
 */
private static class TrustAllHosts implements HostnameVerifier {
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

Open HTTPS connection

Opening HTTPS connection is done with Java’s HttpsURLConnection. Instruction to trust all hosts is done with setHostnameVerifier(new TrustAllHosts()) method. Re-factored code is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public SOAPMessage sendSoapRequest(String endpointUrl, SOAPMessage request) {
    try {
        final boolean isHttps = endpointUrl.toLowerCase().startsWith("https");
        HttpsURLConnection httpsConnection = null;
        // Open HTTPS connection
        if (isHttps) {
            // Open HTTPS connection
            URL url = new URL(endpointUrl);
            httpsConnection = (HttpsURLConnection) url.openConnection();
            // Trust all hosts
            httpsConnection.setHostnameVerifier(new TrustAllHosts());
            // Connect
            httpsConnection.connect();
        }
        // Send HTTP SOAP request and get response
        SOAPConnection soapConnection
                = SOAPConnectionFactory.newInstance().createConnection();
        SOAPMessage response = soapConnection.call(request, endpointUrl);
        // Close connection
        soapConnection.close();
        // Close HTTPS connection
        if (isHttps) {
            httpsConnection.disconnect();
        }
        return response;
    } catch (SOAPException | IOException ex) {
        // Do Something
    }
    return null;
}

Not valid certificate exception

Running code above throws exception which generally means that server is either missing a SSL certificate or its SSL certificate is not valid, i.e. not signed by certification authority. Error is:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
The proper way to handle this is to add server’s certificate to client’s JVM TrustStore certificates. But since test servers may change and server on which client is running may also change generating of certificates is an overhead. Since we are writing test code it is OK to lower the level of security of SSL.

Trust all certificates

Trusting all certificates is a very bad practice and MUST never be used in production code. This is undermining the whole concept and purpose of SSL certificates. For test code is is not that bad to do this sin. A class implementing X509TrustManager interface is needed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * Dummy class implementing X509TrustManager to trust all certificates
 */
private static class TrustAllCertificates implements X509TrustManager {
    public void checkClientTrusted(X509Certificate[] certs, String authType) {
    }
 
    public void checkServerTrusted(X509Certificate[] certs, String authType) {
    }
 
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

Create SSL context trusting all certificates

Instructing HttpsURLConnection to trust all certificates is done with following code:
1
2
3
4
5
6
// Create SSL context and trust all certificates
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManager[] trustAll = new TrustManager[] {new TrustAllCertificates()};
sslContext.init(null, trustAll, new java.security.SecureRandom());
// Set trust all certificates context to HttpsURLConnection
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

Send SOAP over HTTPS without having valid certificates

Final code is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * Sends SOAP request and saves it in a queue.
 *
 * @param request SOAP Message request object
 * @return SOAP Message response object
 */
public SOAPMessage sendSoapRequest(String endpointUrl, SOAPMessage request) {
    try {
        final boolean isHttps = endpointUrl.toLowerCase().startsWith("https");
        HttpsURLConnection httpsConnection = null;
        // Open HTTPS connection
        if (isHttps) {
            // Create SSL context and trust all certificates
            SSLContext sslContext = SSLContext.getInstance("SSL");
            TrustManager[] trustAll
                    = new TrustManager[] {new TrustAllCertificates()};
            sslContext.init(null, trustAll, new java.security.SecureRandom());
            // Set trust all certificates context to HttpsURLConnection
            HttpsURLConnection
                    .setDefaultSSLSocketFactory(sslContext.getSocketFactory());
            // Open HTTPS connection
            URL url = new URL(endpointUrl);
            httpsConnection = (HttpsURLConnection) url.openConnection();
            // Trust all hosts
            httpsConnection.setHostnameVerifier(new TrustAllHosts());
            // Connect
            httpsConnection.connect();
        }
        // Send HTTP SOAP request and get response
        SOAPConnection soapConnection
                = SOAPConnectionFactory.newInstance().createConnection();
        SOAPMessage response = soapConnection.call(request, endpointUrl);
        // Close connection
        soapConnection.close();
        // Close HTTPS connection
        if (isHttps) {
            httpsConnection.disconnect();
        }
        return response;
    } catch (SOAPException | IOException
            | NoSuchAlgorithmException | KeyManagementException ex) {
        // Do Something
    }
    return null;
}

Conclusion

Although this code is very handy and eases a lot testing of SOAP over HTTPS it MUST never be used for production purpose!

Source: http://automationrhapsody.com/send-soap-request-over-https-without-valid-certificates/