Line data Source code
1 : /* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 : /*
3 : Copyright (C) 2011 Red Hat, Inc.
4 :
5 : This library is free software; you can redistribute it and/or
6 : modify it under the terms of the GNU Lesser General Public
7 : License as published by the Free Software Foundation; either
8 : version 2.1 of the License, or (at your option) any later version.
9 :
10 : This library is distributed in the hope that it will be useful,
11 : but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : Lesser General Public License for more details.
14 :
15 : You should have received a copy of the GNU Lesser General Public
16 : License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 : */
18 :
19 : #include <config.h>
20 :
21 : #include "mem.h"
22 : #include "ssl_verify.h"
23 : #include "log.h"
24 :
25 : #ifndef WIN32
26 : #include <sys/socket.h>
27 : #include <netinet/in.h>
28 : #include <arpa/inet.h>
29 : #endif
30 : #include <ctype.h>
31 : #include <string.h>
32 : #include <gio/gio.h>
33 :
34 : #if OPENSSL_VERSION_NUMBER < 0x10100000 || \
35 : (defined (LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000)
36 : static const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *asn1)
37 : {
38 : return M_ASN1_STRING_data(asn1);
39 : }
40 : #endif
41 :
42 0 : static int verify_pubkey(X509* cert, const char *key, size_t key_size)
43 : {
44 0 : EVP_PKEY* cert_pubkey = NULL;
45 0 : EVP_PKEY* orig_pubkey = NULL;
46 0 : BIO* bio = NULL;
47 0 : int ret = 0;
48 :
49 0 : if (!key || key_size == 0)
50 0 : return 0;
51 :
52 0 : if (!cert) {
53 0 : spice_debug("warning: no cert!");
54 0 : return 0;
55 : }
56 :
57 0 : cert_pubkey = X509_get_pubkey(cert);
58 0 : if (!cert_pubkey) {
59 0 : spice_debug("warning: reading public key from certificate failed");
60 0 : goto finish;
61 : }
62 :
63 0 : bio = BIO_new_mem_buf((void*)key, key_size);
64 0 : if (!bio) {
65 0 : spice_debug("creating BIO failed");
66 0 : goto finish;
67 : }
68 :
69 0 : orig_pubkey = d2i_PUBKEY_bio(bio, NULL);
70 0 : if (!orig_pubkey) {
71 0 : spice_debug("reading pubkey from bio failed");
72 0 : goto finish;
73 : }
74 :
75 0 : ret = EVP_PKEY_cmp(orig_pubkey, cert_pubkey);
76 :
77 0 : if (ret == 1) {
78 0 : spice_debug("public keys match");
79 0 : } else if (ret == 0) {
80 0 : spice_debug("public keys mismatch");
81 : } else {
82 0 : spice_debug("public keys types mismatch");
83 : }
84 :
85 0 : finish:
86 0 : if (bio)
87 0 : BIO_free(bio);
88 :
89 0 : if (orig_pubkey)
90 0 : EVP_PKEY_free(orig_pubkey);
91 :
92 0 : if (cert_pubkey)
93 0 : EVP_PKEY_free(cert_pubkey);
94 :
95 0 : return ret;
96 : }
97 :
98 : /* from gnutls
99 : * compare hostname against certificate, taking account of wildcards
100 : * return 1 on success or 0 on error
101 : *
102 : * note: certnamesize is required as X509 certs can contain embedded NULs in
103 : * the strings such as CN or subjectAltName
104 : */
105 0 : static int _gnutls_hostname_compare(const char *certname,
106 : size_t certnamesize, const char *hostname)
107 : {
108 : /* find the first different character */
109 0 : for (; *certname && *hostname && toupper (*certname) == toupper (*hostname);
110 0 : certname++, hostname++, certnamesize--)
111 : ;
112 :
113 : /* the strings are the same */
114 0 : if (certnamesize == 0 && *hostname == '\0')
115 0 : return 1;
116 :
117 0 : if (*certname == '*')
118 : {
119 : /* a wildcard certificate */
120 :
121 0 : certname++;
122 0 : certnamesize--;
123 :
124 : while (1)
125 : {
126 : /* Use a recursive call to allow multiple wildcards */
127 0 : if (_gnutls_hostname_compare (certname, certnamesize, hostname))
128 0 : return 1;
129 :
130 : /* wildcards are only allowed to match a single domain
131 : component or component fragment */
132 0 : if (*hostname == '\0' || *hostname == '.')
133 : break;
134 0 : hostname++;
135 : }
136 :
137 0 : return 0;
138 : }
139 :
140 0 : return 0;
141 : }
142 :
143 : /**
144 : * From gnutls and spice red_peer.c
145 : * TODO: switch to gnutls and get rid of this
146 : *
147 : * This function will check if the given certificate's subject matches
148 : * the given hostname. This is a basic implementation of the matching
149 : * described in RFC2818 (HTTPS), which takes into account wildcards,
150 : * and the DNSName/IPAddress subject alternative name PKIX extension.
151 : *
152 : * Returns: 1 for a successful match, and 0 on failure.
153 : **/
154 0 : static int verify_hostname(X509* cert, const char *hostname)
155 : {
156 : GENERAL_NAMES* subject_alt_names;
157 0 : int found_dns_name = 0;
158 0 : int cn_match = 0;
159 : X509_NAME* subject;
160 :
161 0 : spice_return_val_if_fail(hostname != NULL, 0);
162 :
163 0 : if (!cert) {
164 0 : spice_debug("warning: no cert!");
165 0 : return 0;
166 : }
167 :
168 : /* try matching against:
169 : * 1) a DNS name as an alternative name (subjectAltName) extension
170 : * in the certificate
171 : * 2) the common name (CN) in the certificate
172 : *
173 : * either of these may be of the form: *.domain.tld
174 : *
175 : * only try (2) if there is no subjectAltName extension of
176 : * type dNSName
177 : */
178 :
179 : /* Check through all included subjectAltName extensions, comparing
180 : * against all those of type dNSName.
181 : */
182 0 : subject_alt_names = (GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
183 :
184 0 : if (subject_alt_names) {
185 0 : int num_alts = sk_GENERAL_NAME_num(subject_alt_names);
186 : int i;
187 0 : for (i = 0; i < num_alts; i++) {
188 0 : const GENERAL_NAME* name = sk_GENERAL_NAME_value(subject_alt_names, i);
189 0 : if (name->type == GEN_DNS) {
190 0 : found_dns_name = 1;
191 0 : if (_gnutls_hostname_compare((const char *)ASN1_STRING_get0_data(name->d.dNSName),
192 0 : ASN1_STRING_length(name->d.dNSName),
193 : hostname)) {
194 0 : spice_debug("alt name match=%s", ASN1_STRING_get0_data(name->d.dNSName));
195 0 : GENERAL_NAMES_free(subject_alt_names);
196 0 : return 1;
197 : }
198 0 : } else if (name->type == GEN_IPADD) {
199 : GInetAddress * ip;
200 : const guint8 * ip_binary;
201 : int alt_ip_len;
202 : int ip_len;
203 :
204 0 : found_dns_name = 1;
205 :
206 0 : ip = g_inet_address_new_from_string(hostname);
207 0 : if (ip == NULL) {
208 0 : spice_warning("Could not parse hostname: %s", hostname);
209 0 : continue;
210 : }
211 :
212 0 : ip_len = g_inet_address_get_native_size(ip);
213 0 : ip_binary = g_inet_address_to_bytes(ip);
214 :
215 0 : alt_ip_len = ASN1_STRING_length(name->d.iPAddress);
216 :
217 0 : if ((ip_len == alt_ip_len) &&
218 0 : (memcmp(ASN1_STRING_get0_data(name->d.iPAddress), ip_binary, ip_len)) == 0) {
219 0 : GInetAddress * alt_ip = NULL;
220 0 : gchar * alt_ip_string = NULL;
221 :
222 0 : alt_ip = g_inet_address_new_from_bytes(ASN1_STRING_get0_data(name->d.iPAddress),
223 : g_inet_address_get_family(ip));
224 0 : alt_ip_string = g_inet_address_to_string(alt_ip);
225 0 : spice_debug("alt name IP match=%s", alt_ip_string);
226 :
227 0 : g_free(alt_ip_string);
228 0 : g_object_unref(alt_ip);
229 0 : g_object_unref(ip);
230 0 : GENERAL_NAMES_free(subject_alt_names);
231 0 : return 1;
232 : }
233 0 : g_object_unref(ip);
234 : }
235 : }
236 0 : GENERAL_NAMES_free(subject_alt_names);
237 : }
238 :
239 0 : if (found_dns_name) {
240 0 : spice_debug("warning: SubjectAltName mismatch");
241 0 : return 0;
242 : }
243 :
244 : /* extracting commonNames */
245 0 : subject = X509_get_subject_name(cert);
246 0 : if (subject) {
247 0 : int pos = -1;
248 : X509_NAME_ENTRY* cn_entry;
249 : ASN1_STRING* cn_asn1;
250 :
251 0 : while ((pos = X509_NAME_get_index_by_NID(subject, NID_commonName, pos)) != -1) {
252 0 : cn_entry = X509_NAME_get_entry(subject, pos);
253 0 : if (!cn_entry) {
254 0 : continue;
255 : }
256 0 : cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry);
257 0 : if (!cn_asn1) {
258 0 : continue;
259 : }
260 :
261 0 : if (_gnutls_hostname_compare((const char*)ASN1_STRING_get0_data(cn_asn1),
262 0 : ASN1_STRING_length(cn_asn1),
263 : hostname)) {
264 0 : spice_debug("common name match=%s", (char*)ASN1_STRING_get0_data(cn_asn1));
265 0 : cn_match = 1;
266 0 : break;
267 : }
268 : }
269 : }
270 :
271 0 : if (!cn_match) {
272 0 : spice_debug("warning: common name mismatch");
273 : }
274 :
275 0 : return cn_match;
276 : }
277 :
278 16 : static X509_NAME* subject_to_x509_name(const char *subject, int *nentries)
279 : {
280 : X509_NAME* in_subject;
281 : const char *p;
282 16 : char *key, *val = NULL, *k, *v = NULL;
283 : enum {
284 : KEY,
285 : VALUE
286 : } state;
287 :
288 16 : spice_return_val_if_fail(subject != NULL, NULL);
289 16 : spice_return_val_if_fail(nentries != NULL, NULL);
290 :
291 16 : key = (char*)alloca(strlen(subject)+1);
292 16 : in_subject = X509_NAME_new();
293 :
294 16 : if (!in_subject || !key) {
295 0 : spice_debug("failed to allocate");
296 0 : return NULL;
297 : }
298 :
299 16 : *nentries = 0;
300 :
301 16 : k = key;
302 16 : state = KEY;
303 79 : for (p = subject;; ++p) {
304 79 : int escape = 0;
305 79 : if (*p == '\\') {
306 4 : ++p;
307 4 : if (*p != '\\' && *p != ',') {
308 1 : spice_debug("Invalid character after \\");
309 1 : goto fail;
310 : }
311 3 : escape = 1;
312 : }
313 :
314 78 : switch (state) {
315 49 : case KEY:
316 49 : if (*p == ' ' && k == key) {
317 13 : continue; /* skip spaces before key */
318 36 : } if (*p == 0) {
319 5 : if (k == key) /* empty key, ending */
320 4 : goto success;
321 1 : goto fail;
322 31 : } else if (*p == ',' && !escape) {
323 2 : goto fail; /* assignment is missing */
324 29 : } else if (*p == '=' && !escape) {
325 13 : state = VALUE;
326 13 : *k = 0;
327 13 : val = k + 1;
328 13 : v = val;
329 : } else
330 16 : *k++ = *p;
331 29 : break;
332 29 : case VALUE:
333 29 : if (*p == 0 || (*p == ',' && !escape)) {
334 13 : if (v == val) /* empty value */
335 1 : goto fail;
336 :
337 12 : *v = 0;
338 :
339 12 : if (!X509_NAME_add_entry_by_txt(in_subject, key,
340 : MBSTRING_UTF8,
341 : (const unsigned char*)val,
342 : -1, -1, 0)) {
343 0 : spice_debug("warning: failed to add entry %s=%s to X509_NAME",
344 : key, val);
345 0 : goto fail;
346 : }
347 12 : *nentries += 1;
348 :
349 12 : if (*p == 0)
350 7 : goto success;
351 :
352 5 : state = KEY;
353 5 : k = key;
354 : } else
355 16 : *v++ = *p;
356 21 : break;
357 : }
358 : }
359 :
360 11 : success:
361 11 : return in_subject;
362 :
363 5 : fail:
364 5 : if (in_subject)
365 5 : X509_NAME_free(in_subject);
366 :
367 5 : return NULL;
368 : }
369 :
370 0 : static int verify_subject(X509* cert, SpiceOpenSSLVerify* verify)
371 : {
372 0 : X509_NAME *cert_subject = NULL;
373 : X509_NAME* in_subject;
374 : int ret;
375 : int in_entries;
376 :
377 0 : if (!cert) {
378 0 : spice_debug("warning: no cert!");
379 0 : return 0;
380 : }
381 :
382 0 : cert_subject = X509_get_subject_name(cert);
383 0 : if (!cert_subject) {
384 0 : spice_debug("warning: reading certificate subject failed");
385 0 : return 0;
386 : }
387 :
388 0 : in_subject = subject_to_x509_name(verify->subject, &in_entries);
389 0 : if (!in_subject) {
390 0 : spice_debug("warning: no in_subject!");
391 0 : return 0;
392 : }
393 :
394 : /* Note: this check is redundant with the pre-condition in X509_NAME_cmp */
395 0 : if (X509_NAME_entry_count(cert_subject) != in_entries) {
396 0 : spice_debug("subject mismatch: #entries cert=%d, input=%d",
397 : X509_NAME_entry_count(cert_subject), in_entries);
398 0 : X509_NAME_free(in_subject);
399 0 : return 0;
400 : }
401 :
402 0 : ret = X509_NAME_cmp(cert_subject, in_subject);
403 :
404 0 : if (ret == 0) {
405 0 : spice_debug("subjects match");
406 : } else {
407 : char *p;
408 0 : spice_debug("subjects mismatch");
409 :
410 0 : p = X509_NAME_oneline(cert_subject, NULL, 0);
411 0 : spice_debug("cert_subject: %s", p);
412 0 : free(p);
413 :
414 0 : p = X509_NAME_oneline(in_subject, NULL, 0);
415 0 : spice_debug("in_subject: %s", p);
416 0 : free(p);
417 : }
418 0 : X509_NAME_free(in_subject);
419 :
420 0 : return !ret;
421 : }
422 :
423 0 : static int openssl_verify(int preverify_ok, X509_STORE_CTX *ctx)
424 : {
425 : int depth, err;
426 : SpiceOpenSSLVerify *v;
427 : SSL *ssl;
428 : X509* cert;
429 : char buf[256];
430 : unsigned int failed_verifications;
431 :
432 0 : ssl = (SSL*)X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
433 0 : v = (SpiceOpenSSLVerify*)SSL_get_app_data(ssl);
434 :
435 0 : cert = X509_STORE_CTX_get_current_cert(ctx);
436 0 : X509_NAME_oneline(X509_get_subject_name(cert), buf, 256);
437 0 : depth = X509_STORE_CTX_get_error_depth(ctx);
438 0 : err = X509_STORE_CTX_get_error(ctx);
439 0 : if (depth > 0) {
440 0 : if (!preverify_ok) {
441 0 : spice_warning("Error in certificate chain verification: %s (num=%d:depth%d:%s)",
442 : X509_verify_cert_error_string(err), err, depth, buf);
443 0 : v->all_preverify_ok = 0;
444 :
445 : /* if certificate verification failed, we can still authorize the server */
446 : /* if its public key matches the one we hold in the peer_connect_options. */
447 0 : if (err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN &&
448 0 : v->verifyop & SPICE_SSL_VERIFY_OP_PUBKEY)
449 0 : return 1;
450 :
451 0 : if (err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN)
452 0 : spice_debug("server certificate not being signed by the provided CA");
453 :
454 0 : return 0;
455 : } else
456 0 : return 1;
457 : }
458 :
459 : /* depth == 0 */
460 0 : if (!cert) {
461 0 : spice_debug("failed to get server certificate");
462 0 : return 0;
463 : }
464 :
465 0 : failed_verifications = 0;
466 0 : if (v->verifyop & SPICE_SSL_VERIFY_OP_PUBKEY) {
467 0 : if (verify_pubkey(cert, v->pubkey, v->pubkey_size))
468 0 : return 1;
469 : else
470 0 : failed_verifications |= SPICE_SSL_VERIFY_OP_PUBKEY;
471 : }
472 :
473 0 : if (!preverify_ok) {
474 0 : err = X509_STORE_CTX_get_error(ctx);
475 0 : depth = X509_STORE_CTX_get_error_depth(ctx);
476 0 : spice_warning("Error in server certificate verification: %s (num=%d:depth%d:%s)",
477 : X509_verify_cert_error_string(err), err, depth, buf);
478 0 : return 0;
479 : }
480 0 : if (!v->all_preverify_ok) {
481 0 : return 0;
482 : }
483 :
484 0 : if (v->verifyop & SPICE_SSL_VERIFY_OP_SUBJECT) {
485 0 : if (verify_subject(cert, v))
486 0 : return 1;
487 : else
488 0 : failed_verifications |= SPICE_SSL_VERIFY_OP_SUBJECT;
489 0 : } else if (v->verifyop & SPICE_SSL_VERIFY_OP_HOSTNAME) {
490 0 : if (verify_hostname(cert, v->hostname))
491 0 : return 1;
492 : else
493 0 : failed_verifications |= SPICE_SSL_VERIFY_OP_HOSTNAME;
494 : }
495 :
496 : /* If we reach this code, this means all the tests failed, thus
497 : * verification failed
498 : */
499 0 : if (failed_verifications & SPICE_SSL_VERIFY_OP_PUBKEY)
500 0 : spice_warning("ssl: pubkey verification failed");
501 :
502 0 : if (failed_verifications & SPICE_SSL_VERIFY_OP_HOSTNAME)
503 0 : spice_warning("ssl: hostname '%s' verification failed", v->hostname);
504 :
505 0 : if (failed_verifications & SPICE_SSL_VERIFY_OP_SUBJECT)
506 0 : spice_warning("ssl: subject '%s' verification failed", v->subject);
507 :
508 0 : spice_warning("ssl: verification failed");
509 :
510 0 : return 0;
511 : }
512 :
513 0 : SpiceOpenSSLVerify* spice_openssl_verify_new(SSL *ssl, SPICE_SSL_VERIFY_OP verifyop,
514 : const char *hostname,
515 : const char *pubkey, size_t pubkey_size,
516 : const char *subject)
517 : {
518 : SpiceOpenSSLVerify *v;
519 :
520 0 : if (!verifyop)
521 0 : return NULL;
522 :
523 0 : v = spice_new0(SpiceOpenSSLVerify, 1);
524 :
525 0 : v->ssl = ssl;
526 0 : v->verifyop = verifyop;
527 0 : v->hostname = spice_strdup(hostname);
528 0 : v->pubkey = (char*)spice_memdup(pubkey, pubkey_size);
529 0 : v->pubkey_size = pubkey_size;
530 0 : v->subject = spice_strdup(subject);
531 :
532 0 : v->all_preverify_ok = 1;
533 :
534 0 : SSL_set_app_data(ssl, v);
535 0 : SSL_set_verify(ssl,
536 : SSL_VERIFY_PEER, openssl_verify);
537 :
538 0 : return v;
539 : }
540 :
541 0 : void spice_openssl_verify_free(SpiceOpenSSLVerify* verify)
542 : {
543 0 : if (!verify)
544 0 : return;
545 :
546 0 : free(verify->pubkey);
547 0 : free(verify->subject);
548 0 : free(verify->hostname);
549 :
550 0 : if (verify->ssl)
551 0 : SSL_set_app_data(verify->ssl, NULL);
552 0 : free(verify);
553 : }
|