12

I am trying to figure out how to send a successful HTTP GET request to a server requiring SNI.

I searched on SO and other places, and found some articles that said that SNI is now supported in JDK7, as well as Apache HTTP Components.

https://issues.apache.org/jira/browse/HTTPCLIENT-1119 https://wiki.apache.org/HttpComponents/SNISupport

Relevant SO article: Certificate chain different between HTTPSURLconnection and Apache (System) DefaultHttpClient

--

However, I cannot seem to find any docs that show how to get this to work.

Here is the code I am using...


            KeyStore trustStore  = KeyStore.getInstance(KeyStore.getDefaultType());
            String trustedCertsPath = System.getenv("JAVA_HOME") + "/jre/lib/security/cacerts";
            FileInputStream certstream = new FileInputStream(new File(trustedCertsPath));
            try {
                trustStore.load(certstream, "changeit".toCharArray());
            } finally {
                certstream.close();
            }

        // Trust own CA and all self-signed certs
        SSLContext sslcontext = SSLContexts.custom()
                .loadTrustMaterial(trustStore, new TrustSelfSignedStrategy())
                .build();

        // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[] { "TLSv1" },
                null,
                SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

        CloseableHttpClient httpclient2 = HttpClients.custom()
                .setSSLSocketFactory(sslsf)
                .build();

        CloseableHttpClient httpclient = HttpClients.createDefault();

        HttpGet httpget = new HttpGet(uri);
        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                tempFile = File.createTempFile(httpFile.getTempFilePrefix(), httpFile.getTempFilePosfix());
                FileOutputStream os = new FileOutputStream(tempFile);

                InputStream instream = entity.getContent();
                try {
                    IOUtils.copy(instream, os);
                } finally {
                    try { instream.close(); } catch (Exception e) {}
                    try { os.close(); } catch (Exception e) {}
                }
            }
        } finally {
            response.close();
        }

When I run this, the request fails.

The server requires SNI in the request, and without it, it returns an expired cert that has the wrong CommonName, due to which it gets rejected.

If I use the httpclient2 instance, that is setup using a custom SSL context to allow all hTTP certs, then the request succeeds. However, that is not something I want to enable on a server that does a lot of downloads per day against different hosts.

I am using httpclient v 4.3.5

Any help appreciated.

Thanks.

Community
  • 1
  • 1
feroze
  • 7,380
  • 7
  • 40
  • 57

3 Answers3

10

SNI should work completely transparently when running on Java 1.7 or newer. No configuration is required. If for whatever reason SSL handshake with SNI enabled server fails you should be able to find out why (and whether or not the SNI extension was properly employed) by turning on SSL debug logging as described here [1] and here [2]

'Debugging SSL/TLS Connections' may look outdated but it can still be useful when troubleshooting SSL related issues.

[1] http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/ReadDebug.html

[2] http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#Debug

ok2c
  • 26,450
  • 5
  • 63
  • 71
  • I was running the code through maven. Even though java7 jre was on the path, and enabled, maven was still picking up the old jre6. Dont know why. When I isolated the code into a separate class and ran it through jre7, it worked. – feroze Sep 18 '14 at 00:09
  • 1
    If someone stumble upon this problem in Java7. Make sure someone didn't set `System.setProperty("jsse.enableSNIExtension", "false");`. This need to be removed or set to true. – Nux Oct 17 '20 at 19:45
0

Background: SNI (Server name Identification) was added to TLS v1.2 in 2003 (RFC 3546) then amended in 2006 (RFC 4366) and 2011 (https://www.rfc-editor.org/rfc/rfc6066.html - current). SNI is now part of TLS v1.3 (https://datatracker.ietf.org/doc/html/rfc8446 - published in 2018).

The purpose is NOT to add any additional security neither fix a SSL vulnerability, but help inbound frontends to dispatch incoming sessions to proper servers and select appropriate certificates when multiple servers listen on the same IP address and port (frequent now with CDN's). Henceforth, although still an optional TLS ClientHello (connection opening datagram) extension field labelled "server_name", you can understand that for just the good functioning of the Internet any modern browser or HTTP client library requested to open an HTTPS session do include the server_name field (SNI) into ClientHello's. By a similar token, any modern web server allows configuring multiple virtual hosts on the same IP address and port with different server keys and certificates.

Issues regarding the SNI are linked to unanticipated side uses of the field to spy who connects to whom (confidentiality leakage) and enforce blocking, filtering, or throttling mechanisms, else Denial of Service. The SNI was initially a clear field of ClientHello's and soon became ESNI (Encrypted SNI - easy to defeat at that time) and now an integral part of the Encrypted ClientHello extension fields in TLS v1.3. A good summary of all issues has been published in https://datatracker.ietf.org/doc/rfc8744/.

Checking that a remote server requires the SNI can be easyly achieved with:

# without SNI - the session closes quickly, no certificate visible
openssl s_client -connect remote.server.com:443

# with SNI - the session handshake is complete, you can see the trace of the server certificate (PEM formatted) at stake
openssl s_client -connect remote.server.com:443 -servername remote.server.com

Checking that your HTTP client SW sets the SNI in clientHello's: with Java, you can launch your app or server with the JVM option "-Djavax.net.debug=ssl:handshake" and shall find in traces something like (here for TLS v1.2):

*** ClientHello, TLSv1.2
  RandomCookie: GMT: 1645107779 bytes = { ...
  Session ID: { ...
  Cipher Suites: [ ...
  Compression Methods: { 0 }
  Extension elliptic_curves, curve names: ...
  Extension ec_point_formats, formats: [uncompressed]
  Extension signature_algorithms, signature_algorithms: ...
  Extension server_name, server_name: [type=host_name (0),value=remote.server.com]
***

When the SNI is missing: that is likely entirely bound to the combinations of software libraries and versions at stake. E.g. an Appache HTTP client 4.3 or later, combined with Java JRE 8 or later (fact is, the SSL/TLS implementation is part of the native JSSE package in your JRE / JDK) should do it fine for TLS v1.2 and in case TLS v1.3 support is actually required, you shall use Java 11 or later, else a backport of TLS1.3 into Oracle JDK 8 or Open JDK 8 (see TLSv1.3 - is it available now in Java 8?).

In those case you would need to enforce the SNI over JSSE directly, see for instance:

You may also encounter this BUG (with a custom HostNameVerifier): https://bugs.openjdk.java.net/browse/JDK-8144566

Bernard Hauzeur
  • 2,317
  • 1
  • 18
  • 25
0

Make sure your host name contains a dot, otherwise Java will not send SNI information: https://gamlor.info/posts-output/2019-09-05-java-client-sni/en/

jm009
  • 61
  • 3