mirror of
https://gitlab.uni-freiburg.de/opensourcevdi/spice-common
synced 2025-12-26 14:18:36 +00:00
This should always be defined and including config.h is a requirement. Signed-off-by: Frediano Ziglio <fziglio@redhat.com> Acked-by: Victor Toso <victortoso@redhat.com>
555 lines
16 KiB
C
555 lines
16 KiB
C
/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
/*
|
|
Copyright (C) 2011 Red Hat, Inc.
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include "mem.h"
|
|
#include "ssl_verify.h"
|
|
#include "log.h"
|
|
|
|
#ifndef WIN32
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#endif
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <gio/gio.h>
|
|
|
|
#if OPENSSL_VERSION_NUMBER < 0x10100000 || \
|
|
(defined (LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000)
|
|
static const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *asn1)
|
|
{
|
|
return M_ASN1_STRING_data(asn1);
|
|
}
|
|
#endif
|
|
|
|
static int verify_pubkey(X509* cert, const char *key, size_t key_size)
|
|
{
|
|
EVP_PKEY* cert_pubkey = NULL;
|
|
EVP_PKEY* orig_pubkey = NULL;
|
|
BIO* bio = NULL;
|
|
int ret = 0;
|
|
|
|
if (!key || key_size == 0)
|
|
return 0;
|
|
|
|
if (!cert) {
|
|
spice_debug("warning: no cert!");
|
|
return 0;
|
|
}
|
|
|
|
cert_pubkey = X509_get_pubkey(cert);
|
|
if (!cert_pubkey) {
|
|
spice_debug("warning: reading public key from certificate failed");
|
|
goto finish;
|
|
}
|
|
|
|
bio = BIO_new_mem_buf((void*)key, key_size);
|
|
if (!bio) {
|
|
spice_debug("creating BIO failed");
|
|
goto finish;
|
|
}
|
|
|
|
orig_pubkey = d2i_PUBKEY_bio(bio, NULL);
|
|
if (!orig_pubkey) {
|
|
spice_debug("reading pubkey from bio failed");
|
|
goto finish;
|
|
}
|
|
|
|
ret = EVP_PKEY_cmp(orig_pubkey, cert_pubkey);
|
|
|
|
if (ret == 1) {
|
|
spice_debug("public keys match");
|
|
} else if (ret == 0) {
|
|
spice_debug("public keys mismatch");
|
|
} else {
|
|
spice_debug("public keys types mismatch");
|
|
}
|
|
|
|
finish:
|
|
if (bio)
|
|
BIO_free(bio);
|
|
|
|
if (orig_pubkey)
|
|
EVP_PKEY_free(orig_pubkey);
|
|
|
|
if (cert_pubkey)
|
|
EVP_PKEY_free(cert_pubkey);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* from gnutls
|
|
* compare hostname against certificate, taking account of wildcards
|
|
* return 1 on success or 0 on error
|
|
*
|
|
* note: certnamesize is required as X509 certs can contain embedded NULs in
|
|
* the strings such as CN or subjectAltName
|
|
*/
|
|
static int _gnutls_hostname_compare(const char *certname,
|
|
size_t certnamesize, const char *hostname)
|
|
{
|
|
/* find the first different character */
|
|
for (; *certname && *hostname && toupper (*certname) == toupper (*hostname);
|
|
certname++, hostname++, certnamesize--)
|
|
;
|
|
|
|
/* the strings are the same */
|
|
if (certnamesize == 0 && *hostname == '\0')
|
|
return 1;
|
|
|
|
if (*certname == '*')
|
|
{
|
|
/* a wildcard certificate */
|
|
|
|
certname++;
|
|
certnamesize--;
|
|
|
|
while (1)
|
|
{
|
|
/* Use a recursive call to allow multiple wildcards */
|
|
if (_gnutls_hostname_compare (certname, certnamesize, hostname))
|
|
return 1;
|
|
|
|
/* wildcards are only allowed to match a single domain
|
|
component or component fragment */
|
|
if (*hostname == '\0' || *hostname == '.')
|
|
break;
|
|
hostname++;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* From gnutls and spice red_peer.c
|
|
* TODO: switch to gnutls and get rid of this
|
|
*
|
|
* This function will check if the given certificate's subject matches
|
|
* the given hostname. This is a basic implementation of the matching
|
|
* described in RFC2818 (HTTPS), which takes into account wildcards,
|
|
* and the DNSName/IPAddress subject alternative name PKIX extension.
|
|
*
|
|
* Returns: 1 for a successful match, and 0 on failure.
|
|
**/
|
|
static int verify_hostname(X509* cert, const char *hostname)
|
|
{
|
|
GENERAL_NAMES* subject_alt_names;
|
|
int found_dns_name = 0;
|
|
int cn_match = 0;
|
|
X509_NAME* subject;
|
|
|
|
spice_return_val_if_fail(hostname != NULL, 0);
|
|
|
|
if (!cert) {
|
|
spice_debug("warning: no cert!");
|
|
return 0;
|
|
}
|
|
|
|
/* try matching against:
|
|
* 1) a DNS name as an alternative name (subjectAltName) extension
|
|
* in the certificate
|
|
* 2) the common name (CN) in the certificate
|
|
*
|
|
* either of these may be of the form: *.domain.tld
|
|
*
|
|
* only try (2) if there is no subjectAltName extension of
|
|
* type dNSName
|
|
*/
|
|
|
|
/* Check through all included subjectAltName extensions, comparing
|
|
* against all those of type dNSName.
|
|
*/
|
|
subject_alt_names = (GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
|
|
|
|
if (subject_alt_names) {
|
|
int num_alts = sk_GENERAL_NAME_num(subject_alt_names);
|
|
int i;
|
|
for (i = 0; i < num_alts; i++) {
|
|
const GENERAL_NAME* name = sk_GENERAL_NAME_value(subject_alt_names, i);
|
|
if (name->type == GEN_DNS) {
|
|
found_dns_name = 1;
|
|
if (_gnutls_hostname_compare((const char *)ASN1_STRING_get0_data(name->d.dNSName),
|
|
ASN1_STRING_length(name->d.dNSName),
|
|
hostname)) {
|
|
spice_debug("alt name match=%s", ASN1_STRING_get0_data(name->d.dNSName));
|
|
GENERAL_NAMES_free(subject_alt_names);
|
|
return 1;
|
|
}
|
|
} else if (name->type == GEN_IPADD) {
|
|
GInetAddress * ip = NULL;
|
|
const guint8 * ip_binary = NULL;
|
|
int alt_ip_len = 0;
|
|
int ip_len = 0;
|
|
|
|
found_dns_name = 1;
|
|
|
|
ip = g_inet_address_new_from_string(hostname);
|
|
if (ip != NULL) {
|
|
ip_len = g_inet_address_get_native_size(ip);
|
|
ip_binary = g_inet_address_to_bytes(ip);
|
|
} else {
|
|
spice_warning("Could not parse hostname: %s", hostname);
|
|
}
|
|
|
|
alt_ip_len = ASN1_STRING_length(name->d.iPAddress);
|
|
|
|
if ((ip_len == alt_ip_len) &&
|
|
(memcmp(ASN1_STRING_get0_data(name->d.iPAddress), ip_binary, ip_len)) == 0) {
|
|
GInetAddress * alt_ip = NULL;
|
|
gchar * alt_ip_string = NULL;
|
|
|
|
alt_ip = g_inet_address_new_from_bytes(ASN1_STRING_get0_data(name->d.iPAddress),
|
|
g_inet_address_get_family(ip));
|
|
alt_ip_string = g_inet_address_to_string(alt_ip);
|
|
spice_debug("alt name IP match=%s", alt_ip_string);
|
|
|
|
g_free(alt_ip_string);
|
|
g_object_unref(alt_ip);
|
|
g_object_unref(ip);
|
|
GENERAL_NAMES_free(subject_alt_names);
|
|
return 1;
|
|
}
|
|
if (ip != NULL) {
|
|
g_object_unref(ip);
|
|
}
|
|
}
|
|
}
|
|
GENERAL_NAMES_free(subject_alt_names);
|
|
}
|
|
|
|
if (found_dns_name) {
|
|
spice_debug("warning: SubjectAltName mismatch");
|
|
return 0;
|
|
}
|
|
|
|
/* extracting commonNames */
|
|
subject = X509_get_subject_name(cert);
|
|
if (subject) {
|
|
int pos = -1;
|
|
X509_NAME_ENTRY* cn_entry;
|
|
ASN1_STRING* cn_asn1;
|
|
|
|
while ((pos = X509_NAME_get_index_by_NID(subject, NID_commonName, pos)) != -1) {
|
|
cn_entry = X509_NAME_get_entry(subject, pos);
|
|
if (!cn_entry) {
|
|
continue;
|
|
}
|
|
cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry);
|
|
if (!cn_asn1) {
|
|
continue;
|
|
}
|
|
|
|
if (_gnutls_hostname_compare((const char*)ASN1_STRING_get0_data(cn_asn1),
|
|
ASN1_STRING_length(cn_asn1),
|
|
hostname)) {
|
|
spice_debug("common name match=%s", (char*)ASN1_STRING_get0_data(cn_asn1));
|
|
cn_match = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!cn_match) {
|
|
spice_debug("warning: common name mismatch");
|
|
}
|
|
|
|
return cn_match;
|
|
}
|
|
|
|
static X509_NAME* subject_to_x509_name(const char *subject, int *nentries)
|
|
{
|
|
X509_NAME* in_subject;
|
|
const char *p;
|
|
char *key, *val = NULL, *k, *v = NULL;
|
|
enum {
|
|
KEY,
|
|
VALUE
|
|
} state;
|
|
|
|
spice_return_val_if_fail(subject != NULL, NULL);
|
|
spice_return_val_if_fail(nentries != NULL, NULL);
|
|
|
|
key = (char*)alloca(strlen(subject)+1);
|
|
in_subject = X509_NAME_new();
|
|
|
|
if (!in_subject || !key) {
|
|
spice_debug("failed to allocate");
|
|
return NULL;
|
|
}
|
|
|
|
*nentries = 0;
|
|
|
|
k = key;
|
|
state = KEY;
|
|
for (p = subject;; ++p) {
|
|
int escape = 0;
|
|
if (*p == '\\') {
|
|
++p;
|
|
if (*p != '\\' && *p != ',') {
|
|
spice_debug("Invalid character after \\");
|
|
goto fail;
|
|
}
|
|
escape = 1;
|
|
}
|
|
|
|
switch (state) {
|
|
case KEY:
|
|
if (*p == ' ' && k == key) {
|
|
continue; /* skip spaces before key */
|
|
} if (*p == 0) {
|
|
if (k == key) /* empty key, ending */
|
|
goto success;
|
|
goto fail;
|
|
} else if (*p == ',' && !escape) {
|
|
goto fail; /* assignment is missing */
|
|
} else if (*p == '=' && !escape) {
|
|
state = VALUE;
|
|
*k = 0;
|
|
val = k + 1;
|
|
v = val;
|
|
} else
|
|
*k++ = *p;
|
|
break;
|
|
case VALUE:
|
|
if (*p == 0 || (*p == ',' && !escape)) {
|
|
if (v == val) /* empty value */
|
|
goto fail;
|
|
|
|
*v = 0;
|
|
|
|
if (!X509_NAME_add_entry_by_txt(in_subject, key,
|
|
MBSTRING_UTF8,
|
|
(const unsigned char*)val,
|
|
-1, -1, 0)) {
|
|
spice_debug("warning: failed to add entry %s=%s to X509_NAME",
|
|
key, val);
|
|
goto fail;
|
|
}
|
|
*nentries += 1;
|
|
|
|
if (*p == 0)
|
|
goto success;
|
|
|
|
state = KEY;
|
|
k = key;
|
|
} else
|
|
*v++ = *p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
success:
|
|
return in_subject;
|
|
|
|
fail:
|
|
if (in_subject)
|
|
X509_NAME_free(in_subject);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int verify_subject(X509* cert, SpiceOpenSSLVerify* verify)
|
|
{
|
|
X509_NAME *cert_subject = NULL;
|
|
X509_NAME* in_subject;
|
|
int ret;
|
|
int in_entries;
|
|
|
|
if (!cert) {
|
|
spice_debug("warning: no cert!");
|
|
return 0;
|
|
}
|
|
|
|
cert_subject = X509_get_subject_name(cert);
|
|
if (!cert_subject) {
|
|
spice_debug("warning: reading certificate subject failed");
|
|
return 0;
|
|
}
|
|
|
|
in_subject = subject_to_x509_name(verify->subject, &in_entries);
|
|
if (!in_subject) {
|
|
spice_debug("warning: no in_subject!");
|
|
return 0;
|
|
}
|
|
|
|
/* Note: this check is redundant with the pre-condition in X509_NAME_cmp */
|
|
if (X509_NAME_entry_count(cert_subject) != in_entries) {
|
|
spice_debug("subject mismatch: #entries cert=%d, input=%d",
|
|
X509_NAME_entry_count(cert_subject), in_entries);
|
|
X509_NAME_free(in_subject);
|
|
return 0;
|
|
}
|
|
|
|
ret = X509_NAME_cmp(cert_subject, in_subject);
|
|
|
|
if (ret == 0) {
|
|
spice_debug("subjects match");
|
|
} else {
|
|
char *p;
|
|
spice_debug("subjects mismatch");
|
|
|
|
p = X509_NAME_oneline(cert_subject, NULL, 0);
|
|
spice_debug("cert_subject: %s", p);
|
|
free(p);
|
|
|
|
p = X509_NAME_oneline(in_subject, NULL, 0);
|
|
spice_debug("in_subject: %s", p);
|
|
free(p);
|
|
}
|
|
X509_NAME_free(in_subject);
|
|
|
|
return !ret;
|
|
}
|
|
|
|
static int openssl_verify(int preverify_ok, X509_STORE_CTX *ctx)
|
|
{
|
|
int depth, err;
|
|
SpiceOpenSSLVerify *v;
|
|
SSL *ssl;
|
|
X509* cert;
|
|
char buf[256];
|
|
unsigned int failed_verifications;
|
|
|
|
ssl = (SSL*)X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
|
|
v = (SpiceOpenSSLVerify*)SSL_get_app_data(ssl);
|
|
|
|
cert = X509_STORE_CTX_get_current_cert(ctx);
|
|
X509_NAME_oneline(X509_get_subject_name(cert), buf, 256);
|
|
depth = X509_STORE_CTX_get_error_depth(ctx);
|
|
err = X509_STORE_CTX_get_error(ctx);
|
|
if (depth > 0) {
|
|
if (!preverify_ok) {
|
|
spice_warning("Error in certificate chain verification: %s (num=%d:depth%d:%s)",
|
|
X509_verify_cert_error_string(err), err, depth, buf);
|
|
v->all_preverify_ok = 0;
|
|
|
|
/* if certificate verification failed, we can still authorize the server */
|
|
/* if its public key matches the one we hold in the peer_connect_options. */
|
|
if (err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN &&
|
|
v->verifyop & SPICE_SSL_VERIFY_OP_PUBKEY)
|
|
return 1;
|
|
|
|
if (err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN)
|
|
spice_debug("server certificate not being signed by the provided CA");
|
|
|
|
return 0;
|
|
} else
|
|
return 1;
|
|
}
|
|
|
|
/* depth == 0 */
|
|
if (!cert) {
|
|
spice_debug("failed to get server certificate");
|
|
return 0;
|
|
}
|
|
|
|
failed_verifications = 0;
|
|
if (v->verifyop & SPICE_SSL_VERIFY_OP_PUBKEY) {
|
|
if (verify_pubkey(cert, v->pubkey, v->pubkey_size))
|
|
return 1;
|
|
else
|
|
failed_verifications |= SPICE_SSL_VERIFY_OP_PUBKEY;
|
|
}
|
|
|
|
if (!preverify_ok) {
|
|
err = X509_STORE_CTX_get_error(ctx);
|
|
depth = X509_STORE_CTX_get_error_depth(ctx);
|
|
spice_warning("Error in server certificate verification: %s (num=%d:depth%d:%s)",
|
|
X509_verify_cert_error_string(err), err, depth, buf);
|
|
return 0;
|
|
}
|
|
if (!v->all_preverify_ok) {
|
|
return 0;
|
|
}
|
|
|
|
if (v->verifyop & SPICE_SSL_VERIFY_OP_SUBJECT) {
|
|
if (verify_subject(cert, v))
|
|
return 1;
|
|
else
|
|
failed_verifications |= SPICE_SSL_VERIFY_OP_SUBJECT;
|
|
} else if (v->verifyop & SPICE_SSL_VERIFY_OP_HOSTNAME) {
|
|
if (verify_hostname(cert, v->hostname))
|
|
return 1;
|
|
else
|
|
failed_verifications |= SPICE_SSL_VERIFY_OP_HOSTNAME;
|
|
}
|
|
|
|
/* If we reach this code, this means all the tests failed, thus
|
|
* verification failed
|
|
*/
|
|
if (failed_verifications & SPICE_SSL_VERIFY_OP_PUBKEY)
|
|
spice_warning("ssl: pubkey verification failed");
|
|
|
|
if (failed_verifications & SPICE_SSL_VERIFY_OP_HOSTNAME)
|
|
spice_warning("ssl: hostname '%s' verification failed", v->hostname);
|
|
|
|
if (failed_verifications & SPICE_SSL_VERIFY_OP_SUBJECT)
|
|
spice_warning("ssl: subject '%s' verification failed", v->subject);
|
|
|
|
spice_warning("ssl: verification failed");
|
|
|
|
return 0;
|
|
}
|
|
|
|
SpiceOpenSSLVerify* spice_openssl_verify_new(SSL *ssl, SPICE_SSL_VERIFY_OP verifyop,
|
|
const char *hostname,
|
|
const char *pubkey, size_t pubkey_size,
|
|
const char *subject)
|
|
{
|
|
SpiceOpenSSLVerify *v;
|
|
|
|
if (!verifyop)
|
|
return NULL;
|
|
|
|
v = spice_new0(SpiceOpenSSLVerify, 1);
|
|
|
|
v->ssl = ssl;
|
|
v->verifyop = verifyop;
|
|
v->hostname = spice_strdup(hostname);
|
|
v->pubkey = (char*)spice_memdup(pubkey, pubkey_size);
|
|
v->pubkey_size = pubkey_size;
|
|
v->subject = spice_strdup(subject);
|
|
|
|
v->all_preverify_ok = 1;
|
|
|
|
SSL_set_app_data(ssl, v);
|
|
SSL_set_verify(ssl,
|
|
SSL_VERIFY_PEER, openssl_verify);
|
|
|
|
return v;
|
|
}
|
|
|
|
void spice_openssl_verify_free(SpiceOpenSSLVerify* verify)
|
|
{
|
|
if (!verify)
|
|
return;
|
|
|
|
free(verify->pubkey);
|
|
free(verify->subject);
|
|
free(verify->hostname);
|
|
|
|
if (verify->ssl)
|
|
SSL_set_app_data(verify->ssl, NULL);
|
|
free(verify);
|
|
}
|