qemu/ui/vnc-tls.c
Tim Hardeck 0057a0d590 TLS support for VNC Websockets
Added TLS support to the VNC QEMU Websockets implementation.
VNC-TLS needs to be enabled for this feature to be used.

The required certificates are specified as in case of VNC-TLS
with the VNC parameter "x509=<path>".

If the server certificate isn't signed by a rooth authority it needs to
be manually imported in the browser because at least in case of Firefox
and Chrome there is no user dialog, the connection just gets canceled.

As a side note VEncrypt over Websocket doesn't work atm because TLS can't
be stacked in the current implementation. (It also didn't work before)
Nevertheless to my knowledge there is no HTML 5 VNC client which supports
it and the Websocket connection can be encrypted with regular TLS now so
it should be fine for most use cases.

Signed-off-by: Tim Hardeck <thardeck@suse.de>
Reviewed-by: Anthony Liguori <aliguori@us.ibm.com>
Message-id: 1366727581-5772-1-git-send-email-thardeck@suse.de
Signed-off-by: Anthony Liguori <aliguori@us.ibm.com>
2013-05-03 12:04:57 -05:00

493 lines
15 KiB
C

/*
* QEMU VNC display driver: TLS helpers
*
* Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
* Copyright (C) 2006 Fabrice Bellard
* Copyright (C) 2009 Red Hat, Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "qemu-x509.h"
#include "vnc.h"
#include "qemu/sockets.h"
#if defined(_VNC_DEBUG) && _VNC_DEBUG >= 2
/* Very verbose, so only enabled for _VNC_DEBUG >= 2 */
static void vnc_debug_gnutls_log(int level, const char* str) {
VNC_DEBUG("%d %s", level, str);
}
#endif /* defined(_VNC_DEBUG) && _VNC_DEBUG >= 2 */
#define DH_BITS 1024
static gnutls_dh_params_t dh_params;
static int vnc_tls_initialize(void)
{
static int tlsinitialized = 0;
if (tlsinitialized)
return 1;
if (gnutls_global_init () < 0)
return 0;
/* XXX ought to re-generate diffie-hellman params periodically */
if (gnutls_dh_params_init (&dh_params) < 0)
return 0;
if (gnutls_dh_params_generate2 (dh_params, DH_BITS) < 0)
return 0;
#if defined(_VNC_DEBUG) && _VNC_DEBUG >= 2
gnutls_global_set_log_level(10);
gnutls_global_set_log_function(vnc_debug_gnutls_log);
#endif
tlsinitialized = 1;
return 1;
}
static ssize_t vnc_tls_push(gnutls_transport_ptr_t transport,
const void *data,
size_t len) {
struct VncState *vs = (struct VncState *)transport;
int ret;
retry:
ret = send(vs->csock, data, len, 0);
if (ret < 0) {
if (errno == EINTR)
goto retry;
return -1;
}
return ret;
}
static ssize_t vnc_tls_pull(gnutls_transport_ptr_t transport,
void *data,
size_t len) {
struct VncState *vs = (struct VncState *)transport;
int ret;
retry:
ret = qemu_recv(vs->csock, data, len, 0);
if (ret < 0) {
if (errno == EINTR)
goto retry;
return -1;
}
return ret;
}
static gnutls_anon_server_credentials_t vnc_tls_initialize_anon_cred(void)
{
gnutls_anon_server_credentials_t anon_cred;
int ret;
if ((ret = gnutls_anon_allocate_server_credentials(&anon_cred)) < 0) {
VNC_DEBUG("Cannot allocate credentials %s\n", gnutls_strerror(ret));
return NULL;
}
gnutls_anon_set_server_dh_params(anon_cred, dh_params);
return anon_cred;
}
static gnutls_certificate_credentials_t vnc_tls_initialize_x509_cred(VncDisplay *vd)
{
gnutls_certificate_credentials_t x509_cred;
int ret;
if (!vd->tls.x509cacert) {
VNC_DEBUG("No CA x509 certificate specified\n");
return NULL;
}
if (!vd->tls.x509cert) {
VNC_DEBUG("No server x509 certificate specified\n");
return NULL;
}
if (!vd->tls.x509key) {
VNC_DEBUG("No server private key specified\n");
return NULL;
}
if ((ret = gnutls_certificate_allocate_credentials(&x509_cred)) < 0) {
VNC_DEBUG("Cannot allocate credentials %s\n", gnutls_strerror(ret));
return NULL;
}
if ((ret = gnutls_certificate_set_x509_trust_file(x509_cred,
vd->tls.x509cacert,
GNUTLS_X509_FMT_PEM)) < 0) {
VNC_DEBUG("Cannot load CA certificate %s\n", gnutls_strerror(ret));
gnutls_certificate_free_credentials(x509_cred);
return NULL;
}
if ((ret = gnutls_certificate_set_x509_key_file (x509_cred,
vd->tls.x509cert,
vd->tls.x509key,
GNUTLS_X509_FMT_PEM)) < 0) {
VNC_DEBUG("Cannot load certificate & key %s\n", gnutls_strerror(ret));
gnutls_certificate_free_credentials(x509_cred);
return NULL;
}
if (vd->tls.x509cacrl) {
if ((ret = gnutls_certificate_set_x509_crl_file(x509_cred,
vd->tls.x509cacrl,
GNUTLS_X509_FMT_PEM)) < 0) {
VNC_DEBUG("Cannot load CRL %s\n", gnutls_strerror(ret));
gnutls_certificate_free_credentials(x509_cred);
return NULL;
}
}
gnutls_certificate_set_dh_params (x509_cred, dh_params);
return x509_cred;
}
int vnc_tls_validate_certificate(struct VncState *vs)
{
int ret;
unsigned int status;
const gnutls_datum_t *certs;
unsigned int nCerts, i;
time_t now;
VNC_DEBUG("Validating client certificate\n");
if ((ret = gnutls_certificate_verify_peers2 (vs->tls.session, &status)) < 0) {
VNC_DEBUG("Verify failed %s\n", gnutls_strerror(ret));
return -1;
}
if ((now = time(NULL)) == ((time_t)-1)) {
return -1;
}
if (status != 0) {
if (status & GNUTLS_CERT_INVALID)
VNC_DEBUG("The certificate is not trusted.\n");
if (status & GNUTLS_CERT_SIGNER_NOT_FOUND)
VNC_DEBUG("The certificate hasn't got a known issuer.\n");
if (status & GNUTLS_CERT_REVOKED)
VNC_DEBUG("The certificate has been revoked.\n");
if (status & GNUTLS_CERT_INSECURE_ALGORITHM)
VNC_DEBUG("The certificate uses an insecure algorithm\n");
return -1;
} else {
VNC_DEBUG("Certificate is valid!\n");
}
/* Only support x509 for now */
if (gnutls_certificate_type_get(vs->tls.session) != GNUTLS_CRT_X509)
return -1;
if (!(certs = gnutls_certificate_get_peers(vs->tls.session, &nCerts)))
return -1;
for (i = 0 ; i < nCerts ; i++) {
gnutls_x509_crt_t cert;
VNC_DEBUG ("Checking certificate chain %d\n", i);
if (gnutls_x509_crt_init (&cert) < 0)
return -1;
if (gnutls_x509_crt_import(cert, &certs[i], GNUTLS_X509_FMT_DER) < 0) {
gnutls_x509_crt_deinit (cert);
return -1;
}
if (gnutls_x509_crt_get_expiration_time (cert) < now) {
VNC_DEBUG("The certificate has expired\n");
gnutls_x509_crt_deinit (cert);
return -1;
}
if (gnutls_x509_crt_get_activation_time (cert) > now) {
VNC_DEBUG("The certificate is not yet activated\n");
gnutls_x509_crt_deinit (cert);
return -1;
}
if (gnutls_x509_crt_get_activation_time (cert) > now) {
VNC_DEBUG("The certificate is not yet activated\n");
gnutls_x509_crt_deinit (cert);
return -1;
}
if (i == 0) {
size_t dnameSize = 1024;
vs->tls.dname = g_malloc(dnameSize);
requery:
if ((ret = gnutls_x509_crt_get_dn (cert, vs->tls.dname, &dnameSize)) != 0) {
if (ret == GNUTLS_E_SHORT_MEMORY_BUFFER) {
vs->tls.dname = g_realloc(vs->tls.dname, dnameSize);
goto requery;
}
gnutls_x509_crt_deinit (cert);
VNC_DEBUG("Cannot get client distinguished name: %s",
gnutls_strerror (ret));
return -1;
}
if (vs->vd->tls.x509verify) {
int allow;
if (!vs->vd->tls.acl) {
VNC_DEBUG("no ACL activated, allowing access");
gnutls_x509_crt_deinit (cert);
continue;
}
allow = qemu_acl_party_is_allowed(vs->vd->tls.acl,
vs->tls.dname);
VNC_DEBUG("TLS x509 ACL check for %s is %s\n",
vs->tls.dname, allow ? "allowed" : "denied");
if (!allow) {
gnutls_x509_crt_deinit (cert);
return -1;
}
}
}
gnutls_x509_crt_deinit (cert);
}
return 0;
}
#if defined(GNUTLS_VERSION_NUMBER) && \
GNUTLS_VERSION_NUMBER >= 0x020200 /* 2.2.0 */
static int vnc_set_gnutls_priority(gnutls_session_t s, int x509)
{
const char *priority = x509 ? "NORMAL" : "NORMAL:+ANON-DH";
int rc;
rc = gnutls_priority_set_direct(s, priority, NULL);
if (rc != GNUTLS_E_SUCCESS) {
return -1;
}
return 0;
}
#else
static int vnc_set_gnutls_priority(gnutls_session_t s, int x509)
{
static const int cert_types[] = { GNUTLS_CRT_X509, 0 };
static const int protocols[] = {
GNUTLS_TLS1_1, GNUTLS_TLS1_0, GNUTLS_SSL3, 0
};
static const int kx_anon[] = { GNUTLS_KX_ANON_DH, 0 };
static const int kx_x509[] = {
GNUTLS_KX_DHE_DSS, GNUTLS_KX_RSA,
GNUTLS_KX_DHE_RSA, GNUTLS_KX_SRP, 0
};
int rc;
rc = gnutls_kx_set_priority(s, x509 ? kx_x509 : kx_anon);
if (rc != GNUTLS_E_SUCCESS) {
return -1;
}
rc = gnutls_certificate_type_set_priority(s, cert_types);
if (rc != GNUTLS_E_SUCCESS) {
return -1;
}
rc = gnutls_protocol_set_priority(s, protocols);
if (rc != GNUTLS_E_SUCCESS) {
return -1;
}
return 0;
}
#endif
int vnc_tls_client_setup(struct VncState *vs,
int needX509Creds) {
VncStateTLS *tls;
VNC_DEBUG("Do TLS setup\n");
#ifdef CONFIG_VNC_WS
if (vs->websocket) {
tls = &vs->ws_tls;
} else
#endif /* CONFIG_VNC_WS */
{
tls = &vs->tls;
}
if (vnc_tls_initialize() < 0) {
VNC_DEBUG("Failed to init TLS\n");
vnc_client_error(vs);
return -1;
}
if (tls->session == NULL) {
if (gnutls_init(&tls->session, GNUTLS_SERVER) < 0) {
vnc_client_error(vs);
return -1;
}
if (gnutls_set_default_priority(tls->session) < 0) {
gnutls_deinit(tls->session);
tls->session = NULL;
vnc_client_error(vs);
return -1;
}
if (vnc_set_gnutls_priority(tls->session, needX509Creds) < 0) {
gnutls_deinit(tls->session);
tls->session = NULL;
vnc_client_error(vs);
return -1;
}
if (needX509Creds) {
gnutls_certificate_server_credentials x509_cred = vnc_tls_initialize_x509_cred(vs->vd);
if (!x509_cred) {
gnutls_deinit(tls->session);
tls->session = NULL;
vnc_client_error(vs);
return -1;
}
if (gnutls_credentials_set(tls->session, GNUTLS_CRD_CERTIFICATE, x509_cred) < 0) {
gnutls_deinit(tls->session);
tls->session = NULL;
gnutls_certificate_free_credentials(x509_cred);
vnc_client_error(vs);
return -1;
}
if (vs->vd->tls.x509verify) {
VNC_DEBUG("Requesting a client certificate\n");
gnutls_certificate_server_set_request (tls->session, GNUTLS_CERT_REQUEST);
}
} else {
gnutls_anon_server_credentials_t anon_cred = vnc_tls_initialize_anon_cred();
if (!anon_cred) {
gnutls_deinit(tls->session);
tls->session = NULL;
vnc_client_error(vs);
return -1;
}
if (gnutls_credentials_set(tls->session, GNUTLS_CRD_ANON, anon_cred) < 0) {
gnutls_deinit(tls->session);
tls->session = NULL;
gnutls_anon_free_server_credentials(anon_cred);
vnc_client_error(vs);
return -1;
}
}
gnutls_transport_set_ptr(tls->session, (gnutls_transport_ptr_t)vs);
gnutls_transport_set_push_function(tls->session, vnc_tls_push);
gnutls_transport_set_pull_function(tls->session, vnc_tls_pull);
}
return 0;
}
void vnc_tls_client_cleanup(struct VncState *vs)
{
if (vs->tls.session) {
gnutls_deinit(vs->tls.session);
vs->tls.session = NULL;
}
vs->tls.wiremode = VNC_WIREMODE_CLEAR;
g_free(vs->tls.dname);
#ifdef CONFIG_VNC_WS
if (vs->ws_tls.session) {
gnutls_deinit(vs->ws_tls.session);
vs->ws_tls.session = NULL;
}
vs->ws_tls.wiremode = VNC_WIREMODE_CLEAR;
g_free(vs->ws_tls.dname);
#endif /* CONFIG_VNC_WS */
}
static int vnc_set_x509_credential(VncDisplay *vd,
const char *certdir,
const char *filename,
char **cred,
int ignoreMissing)
{
struct stat sb;
if (*cred) {
g_free(*cred);
*cred = NULL;
}
*cred = g_malloc(strlen(certdir) + strlen(filename) + 2);
strcpy(*cred, certdir);
strcat(*cred, "/");
strcat(*cred, filename);
VNC_DEBUG("Check %s\n", *cred);
if (stat(*cred, &sb) < 0) {
g_free(*cred);
*cred = NULL;
if (ignoreMissing && errno == ENOENT)
return 0;
return -1;
}
return 0;
}
int vnc_tls_set_x509_creds_dir(VncDisplay *vd,
const char *certdir)
{
if (vnc_set_x509_credential(vd, certdir, X509_CA_CERT_FILE, &vd->tls.x509cacert, 0) < 0)
goto cleanup;
if (vnc_set_x509_credential(vd, certdir, X509_CA_CRL_FILE, &vd->tls.x509cacrl, 1) < 0)
goto cleanup;
if (vnc_set_x509_credential(vd, certdir, X509_SERVER_CERT_FILE, &vd->tls.x509cert, 0) < 0)
goto cleanup;
if (vnc_set_x509_credential(vd, certdir, X509_SERVER_KEY_FILE, &vd->tls.x509key, 0) < 0)
goto cleanup;
return 0;
cleanup:
g_free(vd->tls.x509cacert);
g_free(vd->tls.x509cacrl);
g_free(vd->tls.x509cert);
g_free(vd->tls.x509key);
vd->tls.x509cacert = vd->tls.x509cacrl = vd->tls.x509cert = vd->tls.x509key = NULL;
return -1;
}