Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sudo-project
GitHub Repository: sudo-project/sudo
Path: blob/main/lib/iolog/hostcheck.c
1532 views
1
/*
2
* Copyright (c) 2020 Laszlo Orban <[email protected]>
3
*
4
* Permission to use, copy, modify, and distribute this software for any
5
* purpose with or without fee is hereby granted, provided that the above
6
* copyright notice and this permission notice appear in all copies.
7
*
8
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
*/
16
17
#include <config.h>
18
19
#if defined(HAVE_OPENSSL)
20
# if defined(HAVE_WOLFSSL)
21
# include <wolfssl/options.h>
22
# endif
23
# include <sys/types.h>
24
# include <sys/socket.h>
25
# include <netinet/in.h>
26
# include <arpa/inet.h>
27
# include <stdlib.h>
28
# include <string.h>
29
30
# define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */
31
32
# include <sudo_compat.h>
33
# include <sudo_debug.h>
34
# include <sudo_util.h>
35
# include <hostcheck.h>
36
37
#ifndef INET_ADDRSTRLEN
38
# define INET_ADDRSTRLEN 16
39
#endif
40
#ifndef INET6_ADDRSTRLEN
41
# define INET6_ADDRSTRLEN 46
42
#endif
43
44
#if !defined(HAVE_ASN1_STRING_GET0_DATA) && !defined(HAVE_WOLFSSL)
45
# define ASN1_STRING_get0_data(x) ASN1_STRING_data(x)
46
#endif /* !HAVE_ASN1_STRING_GET0_DATA && !HAVE_WOLFSSL */
47
48
/**
49
* @brief Compares the given hostname with a DNS entry in a certificate.
50
*
51
* The certificate DNS name can contain wildcards in the left-most label.
52
* A wildcard can match only one label.
53
* Accepted names:
54
* - foo.bar.example.com
55
* - *.example.com
56
* - *.bar.example.com
57
*
58
* @param hostname peer's name
59
* @param certname_asn1 hostname in the certificate
60
*
61
* @return MatchFound
62
* MatchNotFound
63
* MalformedCertificate
64
*/
65
static HostnameValidationResult
66
validate_name(const char *hostname, ASN1_STRING *certname_asn1)
67
{
68
const char *certname_s = (const char *)ASN1_STRING_get0_data(certname_asn1);
69
size_t certname_len = (size_t)ASN1_STRING_length(certname_asn1);
70
size_t hostname_len = strlen(hostname);
71
debug_decl(validate_name, SUDO_DEBUG_UTIL);
72
73
/* Make sure there isn't an embedded NUL character in certname */
74
if (memchr(certname_s, '\0', certname_len) != NULL) {
75
debug_return_int(MalformedCertificate);
76
}
77
78
/* Remove last '.' from hostname if it exists */
79
if (hostname_len != 0 && hostname[hostname_len - 1] == '.') {
80
--hostname_len;
81
}
82
83
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
84
"comparing %.*s to %.*s in cert", (int)hostname_len, hostname,
85
(int)certname_len, certname_s);
86
87
/* Skip the first label if wildcard */
88
if (certname_len > 2 && certname_s[0] == '*' && certname_s[1] == '.') {
89
while (hostname_len != 0) {
90
--hostname_len;
91
if (*hostname++ == '.') {
92
break;
93
}
94
}
95
certname_s += 2;
96
certname_len -= 2;
97
}
98
99
/* Compare expected hostname with the DNS name */
100
if (certname_len != hostname_len) {
101
debug_return_int(MatchNotFound);
102
}
103
if (strncasecmp(hostname, certname_s, hostname_len) != 0) {
104
debug_return_int(MatchNotFound);
105
}
106
107
debug_return_int(MatchFound);
108
}
109
110
/**
111
* @brief Matches a hostname with the cert's CN.
112
*
113
* @param hostname remote peer's name or NULL if no name
114
* @param cert peer's X509 certificate
115
*
116
* @return MatchFound
117
* MatchNotFound
118
* MalformedCertificate
119
* Error
120
*/
121
static HostnameValidationResult
122
matches_common_name(const char *hostname, X509 *cert)
123
{
124
X509_NAME_ENTRY *common_name_entry = NULL;
125
ASN1_STRING *common_name_asn1 = NULL;
126
int common_name_loc;
127
debug_decl(matches_common_name, SUDO_DEBUG_UTIL);
128
129
if (hostname == NULL) {
130
debug_return_int(MatchNotFound);
131
}
132
133
/* Find the CN field's index in the Subject field of the certificate */
134
common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name(cert),
135
NID_commonName, -1);
136
if (common_name_loc < 0) {
137
debug_return_int(Error);
138
}
139
140
/* Extract the CN field */
141
common_name_entry = X509_NAME_get_entry(X509_get_subject_name(cert),
142
common_name_loc);
143
if (common_name_entry == NULL) {
144
debug_return_int(Error);
145
}
146
147
/* Compare expected hostname with the CN */
148
common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry);
149
if (common_name_asn1 == NULL) {
150
debug_return_int(Error);
151
}
152
if (validate_name(hostname, common_name_asn1) == MatchFound) {
153
debug_return_int(MatchFound);
154
}
155
156
debug_return_int(MatchNotFound);
157
}
158
159
/**
160
* @brief Matches a hostname or ipaddr with the cert's corresponding SAN field.
161
*
162
* SAN can have different fields. For hostname matching, the GEN_DNS field is
163
* used. For IP address matching, the GEN_IPADD field is used.
164
* Since SAN is an X503 v3 extension, it may not be preseent in the cert.
165
*
166
* @param hostname remote peer's name or NULL if no name
167
* @param ipaddr remote peer's IP address
168
* @param cert peer's X509 certificate
169
*
170
* @return MatchFound
171
* MatchNotFound
172
* NoSANPresent
173
* MalformedCertificate
174
* Error
175
*/
176
static HostnameValidationResult
177
matches_subject_alternative_name(const char *hostname, const char *ipaddr,
178
X509 *cert)
179
{
180
HostnameValidationResult ret = MatchNotFound;
181
STACK_OF(GENERAL_NAME) *san_names;
182
int i, san_names_nb;
183
debug_decl(matches_subject_alternative_name, SUDO_DEBUG_UTIL);
184
185
/* Try to extract the names within the SAN extension from the certificate */
186
san_names = X509_get_ext_d2i((X509 *) cert, NID_subject_alt_name, NULL, NULL);
187
if (san_names == NULL) {
188
debug_return_int(NoSANPresent);
189
}
190
san_names_nb = sk_GENERAL_NAME_num(san_names);
191
192
/* Check each name within the extension */
193
for (i = 0; i < san_names_nb; i++) {
194
const GENERAL_NAME *current_name = sk_GENERAL_NAME_value(san_names, i);
195
if (current_name->type == GEN_DNS && hostname != NULL) {
196
/* Compare expected hostname with the DNS name */
197
if (validate_name(hostname, current_name->d.dNSName) == MatchFound) {
198
ret = MatchFound;
199
break;
200
}
201
} else if (current_name->type == GEN_IPADD && ipaddr != NULL) {
202
const unsigned char *san_ip =
203
ASN1_STRING_get0_data(current_name->d.iPAddress);
204
#if defined(HAVE_STRUCT_IN6_ADDR)
205
char san_ip_str[INET6_ADDRSTRLEN];
206
#else
207
char san_ip_str[INET_ADDRSTRLEN];
208
#endif
209
210
/* IPV4 address */
211
if (current_name->d.iPAddress->length == 4) {
212
if (inet_ntop(AF_INET, san_ip, san_ip_str, INET_ADDRSTRLEN) == NULL) {
213
ret = MalformedCertificate;
214
break;
215
}
216
#if defined(HAVE_STRUCT_IN6_ADDR)
217
/* IPV6 address */
218
} else if (current_name->d.iPAddress->length == 16) {
219
if (inet_ntop(AF_INET6, san_ip, san_ip_str, INET6_ADDRSTRLEN) == NULL) {
220
ret = MalformedCertificate;
221
break;
222
}
223
#endif
224
} else {
225
ret = MalformedCertificate;
226
break;
227
}
228
229
if (strcasecmp(ipaddr, san_ip_str) == 0) {
230
ret = MatchFound;
231
break;
232
}
233
}
234
}
235
sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free);
236
237
debug_return_int(ret);
238
}
239
240
/**
241
* @brief Perform hostname/IP validation on the given X509 certificate.
242
*
243
* According to RFC 6125 section 6.4.4, if the SAN field is present, it
244
* must be checked first. The certificate's CN field must only be checked
245
* if no SAN field is present.
246
*
247
* @param cert X509 certificate
248
* @param hostname remote peer's name or NULL if no name
249
* @param ipaddr remote peer's IP address
250
*
251
* @return MatchFound
252
* MatchNotFound
253
* MalformedCertificate
254
* Error
255
*/
256
HostnameValidationResult
257
validate_hostname(X509 *cert, const char *hostname, const char *ipaddr)
258
{
259
HostnameValidationResult ret;
260
debug_decl(validate_hostname, SUDO_DEBUG_UTIL);
261
262
/* Check SAN first if exists */
263
ret = matches_subject_alternative_name(hostname, ipaddr, cert);
264
265
/* RFC 6125 section 6.4.4 says only check CN if no SAN name is present */
266
if (ret == NoSANPresent) {
267
ret = matches_common_name(hostname, cert);
268
}
269
270
debug_return_int(ret);
271
}
272
#endif /* HAVE_OPENSSL */
273
274