FreeTDS/freetds

Unable to validate hostname when hostname is contained in SANs but not equal to CN

FelixGSE opened this issue · 3 comments

Hi everyone,

I am using PyMSSQL to fetch data from an MSSQL server. However, I am facing difficulty in validating the hostname when it is only present in the Subject Alternative Names, and the hostname from the connection does not match the Common Name. I am using PyMSSQL=2.2.8, which is built with FreeTDS==1.3.13 and OpenSSL enabled (see source code build reference). While pinning code references from version 1.3.13, I believe the relevant code in tls.c has not changed, and the same should apply to the latest codebase.

In my connection to a server using bar.com as the hostname (or the server parameter in PyMSSQL connection). The CN of the server certificate is foo.com. The SANs include foo.com and bar.com in that order. When examining the relevant log entries of the dump file, I found the following information:

tls.c:1023:setting default openssl cipher to:HIGH:!SSLv2:!aNULL:-DH
tls.c:147:in tds_push_func_login
tls.c:117:in tds_pull_func_login
tls.c:117:in tds_pull_func_login
tls.c:147:in tds_push_func_login
tls.c:117:in tds_pull_func_login
tls.c:117:in tds_pull_func_login
tls.c:117:in tds_pull_func_login
tls.c:117:in tds_pull_func_login
tls.c:843:Got name foo.con
tls.c:147:in tds_push_func_login
tls.c:1083:checking hostname failed

I am uncertain if the verification process can actually function as intended. Based on my understanding of the code flow, the primary workflow seems to involve the initial invocation of check_names tls.c:1054. Within check_names, check_alt_names is subsequently called to validate the hostname against the entries present in the SANs. If check_alt_names were indeed attempting to validate the alternative names, I would expect to observe log lines tls.c:843: Alt names number 2 and log entries from check_name_match where an attempt to match the SANs against the provided hostname (see tls.c:843 in the dump file.

Based on a careful analysis and based on my understanding it appears that check_alt_names is currently not functioning correctly. Unless there is a significant misunderstanding on my part, it seems that check_alt_names consistently returns -1 whenever the passed hostname parameter is not an IP address (e.g. bar.com). According to the documentation, inet_pton should return 1 if the parameter contains a valid IP address, and 0 otherwise. Therefore, if the hostname argument of check_alt_names is not a valid IP address (e.g. bar.com), check_alt_names returns -1 at this line. Consequently, the subsequent code block responsible for SAN verification in check_alt_names is only reached when the hostname argument is an IP address, which doesn't make sense to me.

If check_alt_names returns -1, the check_names function proceeds to validate the hostname against the CN, which aligns with the behavior I am observing. This validation process of the CN seems to result in the log entry tls.c:843: Got name foo.com. It is this particular section of the check_alt_names function where I see the problem:

static int
check_alt_names(X509 *cert, const char *hostname)
{
    STACK_OF(GENERAL_NAME) *alt_names;
    int i, num;
    int ret = 1;
    union {
        struct in_addr v4;
        struct in6_addr v6;
    } ip;
    unsigned ip_size = 0;
 
    /* check whether @hostname is an ip address */
    if (strchr(hostname, ':') != NULL) {
        ip_size = 16;
        ret = inet_pton(AF_INET6, hostname, &ip.v6);
    } else {
        ip_size = 4;
        ret = inet_pton(AF_INET, hostname, &ip.v4);
    }
    if (ret == 0)
        return -1;
 
    alt_names = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
    if (!alt_names)
        return ret;
 
    num = sk_GENERAL_NAME_num(alt_names);
    tdsdump_log(TDS_DBG_INFO1, "Alt names number %d\n", num);  
    
    ...
 

I compiled a custom script to run check_names in isolation, and when I force the input to be an IP address, I receive the expected log lines mentioned earlier, along with the ability to print the SANs. Additionally, I successfully validated the certificate and hostname using openssl from the command line. When I disable hostname verification then the TLS handshake works fine.

I might be on a completely wrong track here, but any feedback would be greatly appreciated

Cheers

Can you try replacing

        if (ret == 0)
                return -1;

with

        if (ret == 0)
                ip_size = 0;

?

Thanks for the quick response, @freddy77. When I apply that, it seems to work as intended.

When I input a hostname (e.g., 'foo.com'), it ends up in the first block and runs check_name_match on the hostname against the entries in SAN. If I enter an IP address as the hostname parameter, I end up in the else block in check_alt_names (see tls.c:893-tls.cL896)

static int
check_alt_names(X509 *cert, const char *hostname)
{

...

if (name->type == GEN_DNS && ip_size == 0) {
	ret = 0;
	if (!check_name_match(name->d.dNSName, hostname))
		continue;
} else if (name->type == GEN_IPADD && ip_size != 0) {
	ret = 0;
	if (altlen != ip_size || memcmp(altptr, &ip, altlen) != 0)
		continue;
} else {
	continue;
}

...

Fixed in version 1.4.2. Slightly improved (and tested) from the change proposed here.