/* * Copyright (c) 2020 Laszlo Orban * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* * This is an open source non-commercial project. Dear PVS-Studio, please check it. * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com */ #include #if defined(HAVE_OPENSSL) # if defined(HAVE_WOLFSSL) # include # endif # include # include # include # include # include # include # include # define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */ # include "sudo_compat.h" # include "sudo_debug.h" # include "sudo_util.h" # include "hostcheck.h" #ifndef INET_ADDRSTRLEN # define INET_ADDRSTRLEN 16 #endif #ifndef INET6_ADDRSTRLEN # define INET6_ADDRSTRLEN 46 #endif /** * @brief Checks if given hostname resolves to the given IP address. * * @param hostname hostname to be resolved * @param ipaddr ip address to be checked * * @return 1 if hostname resolves to the given IP address * 0 otherwise */ static int forward_lookup_match(const char *hostname, const char *ipaddr) { int rc, ret = 0; struct addrinfo *res = NULL, *p; void *addr; struct sockaddr_in *ipv4; #if defined(HAVE_STRUCT_IN6_ADDR) struct sockaddr_in6 *ipv6; char ipstr[INET6_ADDRSTRLEN]; #else char ipstr[INET_ADDRSTRLEN]; #endif debug_decl(forward_lookup_match, SUDO_DEBUG_UTIL); sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "verify %s resolves to %s", hostname, ipaddr); if ((rc = getaddrinfo(hostname, NULL, NULL, &res)) != 0) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to resolve %s: %s", hostname, gai_strerror(rc)); goto exit; } for (p = res; p != NULL; p = p->ai_next) { if (p->ai_family == AF_INET) { ipv4 = (struct sockaddr_in *)p->ai_addr; addr = &(ipv4->sin_addr); #if defined(HAVE_STRUCT_IN6_ADDR) } else if (p->ai_family == AF_INET6) { ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); #endif } else { goto exit; } if (inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr)) != 0) { sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "comparing %s to %s", ipstr, ipaddr); if (strcmp(ipaddr, ipstr) == 0) { ret = 1; break; } } } exit: if (res != NULL) { freeaddrinfo(res); } debug_return_int(ret); } /** * @brief Compares the given hostname with a DNS entry in a certificate. * * The certificate DNS name can contain wildcards in the left-most label. * A wildcard can match only one label. * Accepted names: * - foo.bar.example.com * - *.example.com * - *.bar.example.com * * @param hostname peer's name * @param certname_asn1 hostname in the certificate * * @return MatchFound * MatchNotFound */ static HostnameValidationResult validate_name(const char *hostname, ASN1_STRING *certname_asn1) { char *certname_s = (char *) ASN1_STRING_get0_data(certname_asn1); int certname_len = ASN1_STRING_length(certname_asn1); int hostname_len = strlen(hostname); debug_decl(validate_name, SUDO_DEBUG_UTIL); /* remove last '.' from hostname if exists */ if (hostname_len != 0 && hostname[hostname_len - 1] == '.') { --hostname_len; } sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "comparing %.*s to %.*s in cert", hostname_len, hostname, certname_len, certname_s); /* skip the first label if wildcard */ if (certname_len > 2 && certname_s[0] == '*' && certname_s[1] == '.') { if (hostname_len != 0) { do { --hostname_len; if (*hostname++ == '.') { break; } } while (hostname_len != 0); } certname_s += 2; certname_len -= 2; } /* Compare expected hostname with the DNS name */ if (certname_len != hostname_len) { debug_return_int(MatchNotFound); } if (strncasecmp(hostname, certname_s, hostname_len) != 0) { debug_return_int(MatchNotFound); } debug_return_int(MatchFound); } /** * @brief Matches a hostname with the cert's CN. * * @param hostname peer's name * on client side: it is the name where the client is connected to * on server side, it is in fact an IP address of the remote client * @param ipaddr peer's IP address * @param cert peer's X509 certificate * @param resolve if the value is not 0, the function checks that the value of the CN * resolves to the given ipaddr or not. * * @return MatchFound * MatchNotFound * MalformedCertificate * Error */ static HostnameValidationResult matches_common_name(const char *hostname, const char *ipaddr, const X509 *cert, int resolve) { X509_NAME_ENTRY *common_name_entry = NULL; ASN1_STRING *common_name_asn1 = NULL; int common_name_loc; debug_decl(matches_common_name, SUDO_DEBUG_UTIL); /* Find the position of the CN field in the Subject field of the certificate */ common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name((X509 *) cert), NID_commonName, -1); if (common_name_loc < 0) { debug_return_int(Error); } /* Extract the CN field */ common_name_entry = X509_NAME_get_entry(X509_get_subject_name((X509 *) cert), common_name_loc); if (common_name_entry == NULL) { debug_return_int(Error); } /* Convert the CN field to a C string */ common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry); if (common_name_asn1 == NULL) { debug_return_int(Error); } const unsigned char *common_name_str = ASN1_STRING_get0_data(common_name_asn1); /* Make sure there isn't an embedded NUL character in the CN */ if (memchr(common_name_str, '\0', ASN1_STRING_length(common_name_asn1)) != NULL) { debug_return_int(MalformedCertificate); } /* Compare expected hostname with the CN */ if (validate_name(hostname, common_name_asn1) == MatchFound) { debug_return_int(MatchFound); } int common_name_length = ASN1_STRING_length(common_name_asn1); char *nullterm_common_name = malloc(common_name_length + 1); if (nullterm_common_name == NULL) { debug_return_int(Error); } memcpy(nullterm_common_name, common_name_str, common_name_length); nullterm_common_name[common_name_length] = '\0'; /* check if hostname in the CN field resolves to the given ip address */ if (resolve && forward_lookup_match(nullterm_common_name, ipaddr)) { free(nullterm_common_name); debug_return_int(MatchFound); } free(nullterm_common_name); debug_return_int(MatchNotFound); } /** * @brief Matches a hostname or ipaddr with the cert's corresponding SAN field. * * SAN can have different fields. For hostname matching, the GEN_DNS field is used, * for IP address matching, the GEN_IPADD field is used. * Since SAN is an X503 v3 extension, it can happen that the cert does * not contain SAN at all. * * @param hostname remote peer's name * on client side: it is the name where the client is connected to * on server side, it is in fact an IP address of the remote client * @param ipaddr remote peer's IP address * @param cert peer's X509 certificate * @param resolve if the value is not 0, the function checks that the value of the * SAN GEN_DNS resolves to the given ipaddr or not. * * @return MatchFound * MatchNotFound * NoSANPresent * MalformedCertificate * Error */ static HostnameValidationResult matches_subject_alternative_name(const char *hostname, const char *ipaddr, const X509 *cert, int resolve) { HostnameValidationResult result = MatchNotFound; int i; int san_names_nb; STACK_OF(GENERAL_NAME) *san_names = NULL; debug_decl(matches_subject_alternative_name, SUDO_DEBUG_UTIL); /* Try to extract the names within the SAN extension from the certificate */ san_names = X509_get_ext_d2i((X509 *) cert, NID_subject_alt_name, NULL, NULL); if (san_names == NULL) { debug_return_int(NoSANPresent); } san_names_nb = sk_GENERAL_NAME_num(san_names); /* Check each name within the extension */ for (i=0; itype == GEN_DNS) { const unsigned char *dns_name = ASN1_STRING_get0_data(current_name->d.dNSName); /* Make sure there isn't an embedded NUL character in the DNS name */ if (memchr(dns_name, '\0', ASN1_STRING_length(current_name->d.dNSName)) != NULL) { result = MalformedCertificate; break; } else { /* Compare expected hostname with the DNS name */ if (validate_name(hostname, current_name->d.dNSName) == MatchFound) { result = MatchFound; break; } int dns_name_length = ASN1_STRING_length(current_name->d.dNSName); char *nullterm_dns_name = malloc(dns_name_length + 1); if (nullterm_dns_name == NULL) { debug_return_int(Error); } memcpy(nullterm_dns_name, dns_name, dns_name_length); nullterm_dns_name[dns_name_length] = '\0'; if (resolve && forward_lookup_match(nullterm_dns_name, ipaddr)) { free(nullterm_dns_name); result = MatchFound; break; } free(nullterm_dns_name); } } else if (current_name->type == GEN_IPADD) { const unsigned char *san_ip = ASN1_STRING_get0_data(current_name->d.iPAddress); #if defined(HAVE_STRUCT_IN6_ADDR) char san_ip_str[INET6_ADDRSTRLEN]; #else char san_ip_str[INET_ADDRSTRLEN]; #endif /* IPV4 address */ if(current_name->d.iPAddress->length == 4) { if (inet_ntop(AF_INET, san_ip, san_ip_str, INET_ADDRSTRLEN) == NULL) { result = MalformedCertificate; break; } #if defined(HAVE_STRUCT_IN6_ADDR) /* IPV6 address */ } else if (current_name->d.iPAddress->length == 16) { if (inet_ntop(AF_INET6, san_ip, san_ip_str, INET6_ADDRSTRLEN) == NULL) { result = MalformedCertificate; break; } # endif } else { result = MalformedCertificate; break; } if (strcasecmp(ipaddr, san_ip_str) == 0) { result = MatchFound; break; } } } sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free); debug_return_int(result); } /** * @brief Do hostname/IP validation on the given X509 certificate. * * According to RFC 6125 section 6.4.4, first the certificate's SAN field * has to be checked. If there is no SAN field, the certificate's CN field * has to be checked. * * @param cert X509 certificate * @param hostname remote peer's name * on client side: it is the name where the client is connected to * on server side, it is in fact an IP address of the remote client * @param ipaddr remote peer's IP address * @param resolve if the value is not 0, the function checks that the value of the * SAN GEN_DNS or the value of CN resolves to the given ipaddr or not. * * @return MatchFound * MatchNotFound * MalformedCertificate * Error */ HostnameValidationResult validate_hostname(const X509 *cert, const char *hostname, const char *ipaddr, int resolve) { HostnameValidationResult res = MatchFound; debug_decl(validate_hostname, SUDO_DEBUG_UTIL); /* hostname can be also an ip address, if client connects * to ip instead of FQDN */ if((ipaddr == NULL) || (cert == NULL)) { debug_return_int(Error); } /* check SAN first if exists */ res = matches_subject_alternative_name(hostname, ipaddr, cert, resolve); /* According to RFC 6125 section 6.4.4, check CN only, * if no SAN name was provided */ if (res == NoSANPresent) { res = matches_common_name(hostname, ipaddr, cert, resolve); } debug_return_int(res); } #endif /* HAVE_OPENSSL */