/* -*- 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 .
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#include "mem.h"
#include "ssl_verify.h"
#ifndef WIN32
#include
#include
#include
#endif
#include
#ifndef SPICE_DEBUG
# define SPICE_DEBUG(format, ...)
#endif
#ifdef WIN32
static int inet_aton(const char* ip, struct in_addr* in_addr)
{
unsigned long addr = inet_addr(ip);
if (addr == INADDR_NONE) {
return 0;
}
in_addr->S_un.S_addr = addr;
return 1;
}
#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;
struct in_addr addr;
int addr_len = 0;
int cn_match = 0;
X509_NAME* subject;
if (!cert) {
SPICE_DEBUG("warning: no cert!");
return 0;
}
// only IpV4 supported
if (inet_aton(hostname, &addr)) {
addr_len = sizeof(struct in_addr);
}
/* 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((char *)ASN1_STRING_data(name->d.dNSName),
ASN1_STRING_length(name->d.dNSName),
hostname)) {
SPICE_DEBUG("alt name match=%s", ASN1_STRING_data(name->d.dNSName));
GENERAL_NAMES_free(subject_alt_names);
return 1;
}
} else if (name->type == GEN_IPADD) {
int alt_ip_len = ASN1_STRING_length(name->d.iPAddress);
found_dns_name = 1;
if ((addr_len == alt_ip_len)&&
!memcmp(ASN1_STRING_data(name->d.iPAddress), &addr, addr_len)) {
SPICE_DEBUG("alt name IP match=%s",
inet_ntoa(*((struct in_addr*)ASN1_STRING_data(name->d.dNSName))));
GENERAL_NAMES_free(subject_alt_names);
return 1;
}
}
}
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((char*)ASN1_STRING_data(cn_asn1),
ASN1_STRING_length(cn_asn1),
hostname)) {
SPICE_DEBUG("common name match=%s", (char*)ASN1_STRING_data(cn_asn1));
cn_match = 1;
break;
}
}
}
if (!cn_match)
SPICE_DEBUG("warning: common name mismatch");
return cn_match;
}
X509_NAME* subject_to_x509_name(const char *subject, int *nentries)
{
X509_NAME* in_subject;
const char *p;
char *key, *val, *k, *v = NULL;
enum {
KEY,
VALUE
} state;
key = (char*)alloca(strlen(subject));
val = (char*)alloca(strlen(subject));
in_subject = X509_NAME_new();
if (!in_subject || !key || !val) {
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;
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;
}
int verify_subject(X509* cert, SpiceOpenSSLVerify* verify)
{
X509_NAME *cert_subject = NULL;
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;
}
if (!verify->in_subject) {
verify->in_subject = subject_to_x509_name(verify->subject, &in_entries);
if (!verify->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);
return 0;
}
ret = X509_NAME_cmp(cert_subject, verify->in_subject);
if (ret == 0)
SPICE_DEBUG("subjects match");
else
SPICE_DEBUG("subjects mismatch");
return !ret;
}
static int openssl_verify(int preverify_ok, X509_STORE_CTX *ctx)
{
int depth;
SpiceOpenSSLVerify *v;
SSL *ssl;
X509* cert;
ssl = (SSL*)X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
v = (SpiceOpenSSLVerify*)SSL_get_app_data(ssl);
depth = X509_STORE_CTX_get_error_depth(ctx);
if (depth > 0) {
if (!preverify_ok) {
SPICE_DEBUG("openssl verify failed at depth=%d", depth);
v->all_preverify_ok = 0;
return 0;
} else
return 1;
}
/* depth == 0 */
cert = X509_STORE_CTX_get_current_cert(ctx);
if (!cert) {
SPICE_DEBUG("failed to get server certificate");
return 0;
}
if (v->verifyop & SPICE_SSL_VERIFY_OP_PUBKEY &&
verify_pubkey(cert, v->pubkey, v->pubkey_size))
return 1;
if (!v->all_preverify_ok || !preverify_ok)
return 0;
if (v->verifyop & SPICE_SSL_VERIFY_OP_HOSTNAME &&
verify_hostname(cert, v->hostname))
return 1;
if (v->verifyop & SPICE_SSL_VERIFY_OP_SUBJECT &&
verify_subject(cert, v))
return 1;
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->in_subject)
X509_NAME_free(verify->in_subject);
if (verify->ssl)
SSL_set_app_data(verify->ssl, NULL);
free(verify);
}