Connecting to a self-signed HTTPS URL in Java

Behrang Saeedzadeh 04 December 2018

A fast and furious brain dump on connecting to HTTPS URLs that use self signed certificates.

Configure HTTPS in Tomcat

  1. Create self signed certificate using keytool:

     $ keytool -genkeypair \
               -keystore tomcat.pkcs12 \
               -dname "CN=Foo, OU=Bar, O=Baz, L=Melbourne, ST=Victoria, C=AU" \
               -keypass changeit \
               -storepass changeit \
               -keyalg RSA \
               -alias tomcat
    
  2. Update Tomcat’s server.xml:

    Enable HTTPS on port 8443 by configuring a connector:

     <Connector protocol="org.apache.coyote.http11.Http11NioProtocol"
                port="8443"
                maxThreads="200"
                scheme="https"
                secure="true"
                SSLEnabled="true"
                keystoreFile="/path/to/tomcat.pkcs12"
                keystorePass="changeit"
                clientAuth="false"
                sslProtocol="TLS"/>
    

    Important attributes here are:

    Name Value
    protocol org.apache.coyote.http11.Http11NioProtocol
    port 8443
    scheme https
    secure true
    SSLEnabled true
    sslProtocol TLS
    clientAuth false
    keystoreFile /path/to/tomcat.pkcs12
    keystorePass changeit
  3. Start Tomcat and verify configuration:

     $ curl -k https://localhost:8443
     $ curl -k https://127.0.0.1:8443
    

Write a Java program to connect to a URL and dump its content

public static void catUrl(final URL url) {
    try (final InputStream inputStream = url.openStream()) {
        // inputStream#readAllBytes was added in Java 9
        final String dump = new String(inputStream.readAllBytes());
        System.out.println(dump);
    } catch (IOException ioe) {
        System.err.println("Could not cat: " + url);
        ioe.printStackTrace(System.err);
    }
}

For https://localhost:8443, it will fail with an exception:

javax.net.ssl.SSLHandshakeException:
    PKIX path building failed:
            sun.security.provider.certpath.SunCertPathBuilderException:
                unable to find valid certification path to requested target

This exception is thrown as the certificate is self signed and it is not trusted by Java. The same exception is thrown if we connect to Tomcat using its IP address (i.e. https://127.0.0.1:8443). To fix this, we should create a trust store that contains Tomcat’s self-signed public certificate and configure our Java program to use it instead of its default trust store.

Export the public certificate from /path/to/tomcat.pkcs12:

$ keytool -export \
          -alias tomcat \
          -storepass changeit \
          -file tomcat.cer \
          -keystore /path/to/tomcat.pkcs12

Create a new cacerts file and add tomcat.cer to it:

$ keytool -import \
          -v \
          -trustcacerts \
          -alias tomcat \
          -file tomcat.cer \
          -keystore tomcat.cacerts.jks \
          -keypass changeit \
          -storepass changeit

Pass tomcat.cacerts.jks as a system property to the program and run it again:

System.setProperty("javax.net.ssl.trustStore", "tomcat.cacerts.jks");

It will fail again, this time with another exception:

javax.net.ssl.SSLException: Unexpected error:
        java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty

Very unhelpful error message: as we are using a custom trust store, we should also pass its password and type as system properties:

System.setProperty("javax.net.ssl.trustStore", "tomcat.cacerts.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
System.setProperty("javax.net.ssl.trustStoreType", "JKS");

Rerun will fail again this time with yet another exception:

javax.net.ssl.SSLHandshakeException: No name matching localhost found

This happens, because our certificate is not associated with our domain name, in this case localhost. We should recreate the certificate and add a Subject Alternate Name to it:

$ keytool -genkeypair \
          -keystore tomcat.pkcs12 \
          -dname "CN=Foo, OU=Bar, O=Baz, L=Melbourne, ST=Victoria, C=AU" \
          -keypass changeit \
          -storepass changeit \
          -keyalg RSA \
          -alias tomcat \
          -ext SAN=dns:localhost

Recreate tomcat.cacerts.jks with this new certificate and rerun the program. This time it will successfully dump the URL. Yay!

Now if we point the Java program at https://127.0.0.1:8443 to dump it, it will fail again:

javax.net.ssl.SSLHandshakeException:
    No subject alternative names matching IP address 127.0.0.1 found

This happens because we had only added the localhost SAN. Let’s recreate the certificate once again, this time with an additional SAN of 127.0.0.1:

$ keytool -genkeypair \
          -keystore tomcat.pkcs12 \
          -dname "CN=Foo, OU=Bar, O=Baz, L=Melbourne, ST=Victoria, C=AU" \
          -keypass changeit \
          -storepass changeit \
          -keyalg RSA \
          -alias tomcat \
          -ext SAN=dns:localhost,ip:127.0.0.1

Recreate the cacerts file and rerun the program. It should successfully dump the contents this time. Yay++!

Alternative option: disable SSL verification in Java

We can create a custom trust all trust manager to skip checking the validity of server certificates, but this is unsafe and should be avoided in real-world use cases.

SSLContext context = SSLContext.getInstance("TLS");

context.init(null, new TrustManager[]{new X509ExtendedTrustManager() {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
    throws CertificateException {
        // empty
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
    throws CertificateException {
        // empty
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
    throws CertificateException {
        // empty
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
    throws CertificateException {
        // empty
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
    throws CertificateException {
        // empty
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
    throws CertificateException {
        // empty
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}}, null);

HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());