1*657871a7Schristos /* $NetBSD: openssl_hostname_validation.c,v 1.1.1.2 2021/04/07 02:43:15 christos Exp $ */
2805a1ce9Schristos /* Obtained from: https://github.com/iSECPartners/ssl-conservatory */
3805a1ce9Schristos
4805a1ce9Schristos /*
5805a1ce9Schristos Copyright (C) 2012, iSEC Partners.
6805a1ce9Schristos
7805a1ce9Schristos Permission is hereby granted, free of charge, to any person obtaining a copy of
8805a1ce9Schristos this software and associated documentation files (the "Software"), to deal in
9805a1ce9Schristos the Software without restriction, including without limitation the rights to
10805a1ce9Schristos use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11805a1ce9Schristos of the Software, and to permit persons to whom the Software is furnished to do
12805a1ce9Schristos so, subject to the following conditions:
13805a1ce9Schristos
14805a1ce9Schristos The above copyright notice and this permission notice shall be included in all
15805a1ce9Schristos copies or substantial portions of the Software.
16805a1ce9Schristos
17805a1ce9Schristos THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18805a1ce9Schristos IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19805a1ce9Schristos FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20805a1ce9Schristos AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21805a1ce9Schristos LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22805a1ce9Schristos OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23805a1ce9Schristos SOFTWARE.
24805a1ce9Schristos */
25805a1ce9Schristos
26805a1ce9Schristos /*
27805a1ce9Schristos * Helper functions to perform basic hostname validation using OpenSSL.
28805a1ce9Schristos *
29805a1ce9Schristos * Please read "everything-you-wanted-to-know-about-openssl.pdf" before
30805a1ce9Schristos * attempting to use this code. This whitepaper describes how the code works,
31805a1ce9Schristos * how it should be used, and what its limitations are.
32805a1ce9Schristos *
33805a1ce9Schristos * Author: Alban Diquet
34805a1ce9Schristos * License: See LICENSE
35805a1ce9Schristos *
36805a1ce9Schristos */
37805a1ce9Schristos
38805a1ce9Schristos // Get rid of OSX 10.7 and greater deprecation warnings.
39805a1ce9Schristos #if defined(__APPLE__) && defined(__clang__)
40805a1ce9Schristos #pragma clang diagnostic ignored "-Wdeprecated-declarations"
41805a1ce9Schristos #endif
42805a1ce9Schristos
43805a1ce9Schristos #include <openssl/x509v3.h>
44805a1ce9Schristos #include <openssl/ssl.h>
45805a1ce9Schristos #include <string.h>
46805a1ce9Schristos
47805a1ce9Schristos #include "openssl_hostname_validation.h"
48805a1ce9Schristos #include "hostcheck.h"
49805a1ce9Schristos
50805a1ce9Schristos #define HOSTNAME_MAX_SIZE 255
51805a1ce9Schristos
52*657871a7Schristos #if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \
53*657871a7Schristos (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L)
54805a1ce9Schristos #define ASN1_STRING_get0_data ASN1_STRING_data
55805a1ce9Schristos #endif
56805a1ce9Schristos
57805a1ce9Schristos /**
58805a1ce9Schristos * Tries to find a match for hostname in the certificate's Common Name field.
59805a1ce9Schristos *
60805a1ce9Schristos * Returns MatchFound if a match was found.
61805a1ce9Schristos * Returns MatchNotFound if no matches were found.
62805a1ce9Schristos * Returns MalformedCertificate if the Common Name had a NUL character embedded in it.
63805a1ce9Schristos * Returns Error if the Common Name could not be extracted.
64805a1ce9Schristos */
matches_common_name(const char * hostname,const X509 * server_cert)65805a1ce9Schristos static HostnameValidationResult matches_common_name(const char *hostname, const X509 *server_cert) {
66805a1ce9Schristos int common_name_loc = -1;
67805a1ce9Schristos X509_NAME_ENTRY *common_name_entry = NULL;
68805a1ce9Schristos ASN1_STRING *common_name_asn1 = NULL;
69805a1ce9Schristos const char *common_name_str = NULL;
70805a1ce9Schristos
71805a1ce9Schristos // Find the position of the CN field in the Subject field of the certificate
72805a1ce9Schristos common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name((X509 *) server_cert), NID_commonName, -1);
73805a1ce9Schristos if (common_name_loc < 0) {
74805a1ce9Schristos return Error;
75805a1ce9Schristos }
76805a1ce9Schristos
77805a1ce9Schristos // Extract the CN field
78805a1ce9Schristos common_name_entry = X509_NAME_get_entry(X509_get_subject_name((X509 *) server_cert), common_name_loc);
79805a1ce9Schristos if (common_name_entry == NULL) {
80805a1ce9Schristos return Error;
81805a1ce9Schristos }
82805a1ce9Schristos
83805a1ce9Schristos // Convert the CN field to a C string
84805a1ce9Schristos common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry);
85805a1ce9Schristos if (common_name_asn1 == NULL) {
86805a1ce9Schristos return Error;
87805a1ce9Schristos }
88805a1ce9Schristos common_name_str = (char *) ASN1_STRING_get0_data(common_name_asn1);
89805a1ce9Schristos
90805a1ce9Schristos // Make sure there isn't an embedded NUL character in the CN
91805a1ce9Schristos if ((size_t)ASN1_STRING_length(common_name_asn1) != strlen(common_name_str)) {
92805a1ce9Schristos return MalformedCertificate;
93805a1ce9Schristos }
94805a1ce9Schristos
95805a1ce9Schristos // Compare expected hostname with the CN
96805a1ce9Schristos if (Curl_cert_hostcheck(common_name_str, hostname) == CURL_HOST_MATCH) {
97805a1ce9Schristos return MatchFound;
98805a1ce9Schristos }
99805a1ce9Schristos else {
100805a1ce9Schristos return MatchNotFound;
101805a1ce9Schristos }
102805a1ce9Schristos }
103805a1ce9Schristos
104805a1ce9Schristos
105805a1ce9Schristos /**
106805a1ce9Schristos * Tries to find a match for hostname in the certificate's Subject Alternative Name extension.
107805a1ce9Schristos *
108805a1ce9Schristos * Returns MatchFound if a match was found.
109805a1ce9Schristos * Returns MatchNotFound if no matches were found.
110805a1ce9Schristos * Returns MalformedCertificate if any of the hostnames had a NUL character embedded in it.
111805a1ce9Schristos * Returns NoSANPresent if the SAN extension was not present in the certificate.
112805a1ce9Schristos */
matches_subject_alternative_name(const char * hostname,const X509 * server_cert)113805a1ce9Schristos static HostnameValidationResult matches_subject_alternative_name(const char *hostname, const X509 *server_cert) {
114805a1ce9Schristos HostnameValidationResult result = MatchNotFound;
115805a1ce9Schristos int i;
116805a1ce9Schristos int san_names_nb = -1;
117805a1ce9Schristos STACK_OF(GENERAL_NAME) *san_names = NULL;
118805a1ce9Schristos
119805a1ce9Schristos // Try to extract the names within the SAN extension from the certificate
120805a1ce9Schristos san_names = X509_get_ext_d2i((X509 *) server_cert, NID_subject_alt_name, NULL, NULL);
121805a1ce9Schristos if (san_names == NULL) {
122805a1ce9Schristos return NoSANPresent;
123805a1ce9Schristos }
124805a1ce9Schristos san_names_nb = sk_GENERAL_NAME_num(san_names);
125805a1ce9Schristos
126805a1ce9Schristos // Check each name within the extension
127805a1ce9Schristos for (i=0; i<san_names_nb; i++) {
128805a1ce9Schristos const GENERAL_NAME *current_name = sk_GENERAL_NAME_value(san_names, i);
129805a1ce9Schristos
130805a1ce9Schristos if (current_name->type == GEN_DNS) {
131805a1ce9Schristos // Current name is a DNS name, let's check it
132805a1ce9Schristos const char *dns_name = (char *) ASN1_STRING_get0_data(current_name->d.dNSName);
133805a1ce9Schristos
134805a1ce9Schristos // Make sure there isn't an embedded NUL character in the DNS name
135805a1ce9Schristos if ((size_t)ASN1_STRING_length(current_name->d.dNSName) != strlen(dns_name)) {
136805a1ce9Schristos result = MalformedCertificate;
137805a1ce9Schristos break;
138805a1ce9Schristos }
139805a1ce9Schristos else { // Compare expected hostname with the DNS name
140805a1ce9Schristos if (Curl_cert_hostcheck(dns_name, hostname)
141805a1ce9Schristos == CURL_HOST_MATCH) {
142805a1ce9Schristos result = MatchFound;
143805a1ce9Schristos break;
144805a1ce9Schristos }
145805a1ce9Schristos }
146805a1ce9Schristos }
147805a1ce9Schristos }
148805a1ce9Schristos sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free);
149805a1ce9Schristos
150805a1ce9Schristos return result;
151805a1ce9Schristos }
152805a1ce9Schristos
153805a1ce9Schristos
154805a1ce9Schristos /**
155805a1ce9Schristos * Validates the server's identity by looking for the expected hostname in the
156805a1ce9Schristos * server's certificate. As described in RFC 6125, it first tries to find a match
157805a1ce9Schristos * in the Subject Alternative Name extension. If the extension is not present in
158805a1ce9Schristos * the certificate, it checks the Common Name instead.
159805a1ce9Schristos *
160805a1ce9Schristos * Returns MatchFound if a match was found.
161805a1ce9Schristos * Returns MatchNotFound if no matches were found.
162805a1ce9Schristos * Returns MalformedCertificate if any of the hostnames had a NUL character embedded in it.
163805a1ce9Schristos * Returns Error if there was an error.
164805a1ce9Schristos */
validate_hostname(const char * hostname,const X509 * server_cert)165805a1ce9Schristos HostnameValidationResult validate_hostname(const char *hostname, const X509 *server_cert) {
166805a1ce9Schristos HostnameValidationResult result;
167805a1ce9Schristos
168805a1ce9Schristos if((hostname == NULL) || (server_cert == NULL))
169805a1ce9Schristos return Error;
170805a1ce9Schristos
171805a1ce9Schristos // First try the Subject Alternative Names extension
172805a1ce9Schristos result = matches_subject_alternative_name(hostname, server_cert);
173805a1ce9Schristos if (result == NoSANPresent) {
174805a1ce9Schristos // Extension was not found: try the Common Name
175805a1ce9Schristos result = matches_common_name(hostname, server_cert);
176805a1ce9Schristos }
177805a1ce9Schristos
178805a1ce9Schristos return result;
179805a1ce9Schristos }
180