3
votes

I am writing a function to verify if the hostname/CN in a certificate matches the hostname in the url.

My setup: I am Using default SSLSockets provided by Java. I have added a HandshakeCompletedListener to the SSLSocket. (This is just me protoyping a solution. I don't think that its the best way to verify certificates after the handshake is completed)

My Conundrum: when I connect to https://gmail.com i.e. Host: gmail.com Port: 443 over ssl, I get a certificate for CN=mail.google.com. My hostname verification function rejects this certificate and closes the connection.

Strangely, widely used browsers don't do the same. They don't display the usual message "certificate is not trusted. do you want to proceed?" Somehow, they all trust the certificate presented to them even though it doesn't match the hostname in the url.

So what is the browser doing, such that it doesn't reject the certificate outright? What extra steps is it taking to make sure that the certificate becomes valid along the way? By which I mean, after a series of redirects https://gmail.com gets replaced by https://mail.google.com and the certificate validates without any issue since it now matches CN=mail.google.com. Are there any specific rules behind this mechanism?

I would like to hear any ideas that you may have. :)

EDIT: I have included a test program that prints out the certificates sent by the host/peer. And also prints out the http message. The program sends a get request to gmail.com. I still see the CN in the certificate as CN=mail.google.com. Anyone care to test this out? I find this behaviour strange because curl -v -k https://gmail.com, as suggested by ian in his comment, returns an entirely different result.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.cert.Certificate;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;


public class SSLCheck {

public static String[] supportedCiphers = {"SSL_RSA_WITH_RC4_128_MD5",
                                        "SSL_RSA_WITH_RC4_128_SHA",
                                        "TLS_RSA_WITH_AES_128_CBC_SHA",
                                        "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
                                        "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
                                        "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
                                        "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
                                        "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
                                        "SSL_RSA_WITH_DES_CBC_SHA",
                                        "SSL_DHE_RSA_WITH_DES_CBC_SHA",
                                        "SSL_DHE_DSS_WITH_DES_CBC_SHA",
                                        "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
                                        "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
                                        "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
                                        "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
                                        "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"};

public static void main(String[] args) {
    int port = 443;
    String host = "gmail.com";

    try {
        Socket sock = SSLSocketFactory.getDefault().createSocket(host, port);
        sock.setSoTimeout(2000);
        ((SSLSocket)sock).setEnabledCipherSuites(supportedCiphers);

        PrintWriter out = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));

        out.println("GET " + "/mail" + " HTTP/1.1");
        out.println("Host: "+host);
        out.println("Accept: */*");
        out.println();
        out.flush();

        BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));

        SSLSocket ssls = (SSLSocket)sock;
        Certificate[] peercerts = ssls.getSession().getPeerCertificates();

        System.out.println("***********************PEER CERTS**********************");
        for(int i=0;i<peercerts.length;i++){
            System.out.println(peercerts[i]);       
            System.out.println("*********************************************************");
        }

        String line;
        while ((line = in.readLine())!=null) {
            System.out.println(line);
        }

        out.close();
        in.close();

    } catch (UnknownHostException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally { 
        System.exit(0);
    }
}

}
2

2 Answers

3
votes

The certificate you get without using the Server Name Indication extension is indeed for CN=mail.google.com, without any Subject Alternative Name for gmail.com.

If you try with SNI, you'll get a different cert, for gmail.com:

openssl s_client -connect gmail.com:443 -servername gmail.com | openssl x509 -text -noout

Most modern version of browsers on desktop support SNI. On the desktop, the main remaining one not supporting SNI is IE on any version of XP. Since it's a TLS extension that hasn't been backported to SSLv3, this also explains why it doesn't work with curl -3.

Support for SNI in Java is only available since Java 7 (and only on the client side).

(By the way, unless you know what you're doing, leave the default enabled cipher suites, in particular, don't enable weak ones like the EXPORT or pseudo-suites like TLS_EMPTY_RENEGOTIATION_INFO_SCSV.)

2
votes

A quick test with curl -v tells me that the initial connection to https://gmail.com presents a certificate with CN=gmail.com. The HTTP response inside the SSL envelope is a 301 redirect to https://mail.google.com/mail/, which in turn presents a CN=mail.google.com certificate. So in this case the CNs do in fact match at every stage.

But in general, you need to learn about the "subject alternative name" extension, which is a way for a certificate to specify a number of different host names that it is valid for in addition to the main CN.