qemu/vnc-auth-sasl.c

627 lines
17 KiB
C
Raw Normal View History

Add SASL authentication support ("Daniel P. Berrange") This patch adds the new SASL authentication protocol to the VNC server. It is enabled by setting the 'sasl' flag when launching VNC. SASL can optionally provide encryption via its SSF layer, if a suitable mechanism is configured (eg, GSSAPI/Kerberos, or Digest-MD5). If an SSF layer is not available, then it should be combined with the x509 VNC authentication protocol which provides encryption. eg, if using GSSAPI qemu -vnc localhost:1,sasl eg if using TLS/x509 for encryption qemu -vnc localhost:1,sasl,tls,x509 By default the Cyrus SASL library will look for its configuration in the file /etc/sasl2/qemu.conf. For non-root users, this can be overridden by setting the SASL_CONF_PATH environment variable, eg to make it look in $HOME/.sasl2. NB unprivileged users may not have access to the full range of SASL mechanisms, since some of them require some administrative privileges to configure. The patch includes an example SASL configuration file which illustrates config for GSSAPI and Digest-MD5, though it should be noted that the latter is not really considered secure any more. Most of the SASL authentication code is located in a separate source file, vnc-auth-sasl.c. The main vnc.c file only contains minimal integration glue, specifically parsing of command line flags / setup, and calls to start the SASL auth process, to do encoding/decoding for data. There are several possible stacks for reading & writing of data, depending on the combo of VNC authentication methods in use - Clear. read/write straight to socket - TLS. read/write via GNUTLS helpers - SASL. encode/decode via SASL SSF layer, then read/write to socket - SASL+TLS. encode/decode via SASL SSF layer, then read/write via GNUTLS Hence, the vnc_client_read & vnc_client_write methods have been refactored a little. vnc_client_read: main entry point for reading, calls either - vnc_client_read_plain reading, with no intermediate decoding - vnc_client_read_sasl reading, with SASL SSF decoding These two methods, then call vnc_client_read_buf(). This decides whether to write to the socket directly or write via GNUTLS. The situation is the same for writing data. More extensive comments have been added in the code / patch. The vnc_client_read_sasl and vnc_client_write_sasl method implementations live in the separate vnc-auth-sasl.c file. The state required for the SASL auth mechanism is kept in a separate VncStateSASL struct, defined in vnc-auth-sasl.h and included in the main VncState. The configure script probes for SASL and automatically enables it if found, unless --disable-vnc-sasl was given to override it. Makefile | 7 Makefile.target | 5 b/qemu.sasl | 34 ++ b/vnc-auth-sasl.c | 626 ++++++++++++++++++++++++++++++++++++++++++++++++++++ b/vnc-auth-sasl.h | 67 +++++ configure | 34 ++ qemu-doc.texi | 97 ++++++++ vnc-auth-vencrypt.c | 12 vnc.c | 249 ++++++++++++++++++-- vnc.h | 31 ++ 10 files changed, 1129 insertions(+), 33 deletions(-) Signed-off-by: Daniel P. Berrange <berrange@redhat.com> Signed-off-by: Anthony Liguori <aliguori@us.ibm.com> git-svn-id: svn://svn.savannah.nongnu.org/qemu/trunk@6724 c046a42c-6fe2-441c-8c8c-71466251a162
2009-03-07 04:27:28 +08:00
/*
* QEMU VNC display driver: SASL auth protocol
*
* 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 "vnc.h"
/* Max amount of data we send/recv for SASL steps to prevent DOS */
#define SASL_DATA_MAX_LEN (1024 * 1024)
void vnc_sasl_client_cleanup(VncState *vs)
{
if (vs->sasl.conn) {
vs->sasl.runSSF = vs->sasl.waitWriteSSF = vs->sasl.wantSSF = 0;
vs->sasl.encodedLength = vs->sasl.encodedOffset = 0;
vs->sasl.encoded = NULL;
free(vs->sasl.username);
free(vs->sasl.mechlist);
vs->sasl.username = vs->sasl.mechlist = NULL;
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
}
}
long vnc_client_write_sasl(VncState *vs)
{
long ret;
VNC_DEBUG("Write SASL: Pending output %p size %d offset %d Encoded: %p size %d offset %d\n",
vs->output.buffer, vs->output.capacity, vs->output.offset,
vs->sasl.encoded, vs->sasl.encodedLength, vs->sasl.encodedOffset);
if (!vs->sasl.encoded) {
int err;
err = sasl_encode(vs->sasl.conn,
(char *)vs->output.buffer,
vs->output.offset,
(const char **)&vs->sasl.encoded,
&vs->sasl.encodedLength);
if (err != SASL_OK)
return vnc_client_io_error(vs, -1, EIO);
vs->sasl.encodedOffset = 0;
}
ret = vnc_client_write_buf(vs,
vs->sasl.encoded + vs->sasl.encodedOffset,
vs->sasl.encodedLength - vs->sasl.encodedOffset);
if (!ret)
return 0;
vs->sasl.encodedOffset += ret;
if (vs->sasl.encodedOffset == vs->sasl.encodedLength) {
vs->output.offset = 0;
vs->sasl.encoded = NULL;
vs->sasl.encodedOffset = vs->sasl.encodedLength = 0;
}
/* Can't merge this block with one above, because
* someone might have written more unencrypted
* data in vs->output while we were processing
* SASL encoded output
*/
if (vs->output.offset == 0) {
qemu_set_fd_handler2(vs->csock, NULL, vnc_client_read, NULL, vs);
}
return ret;
}
long vnc_client_read_sasl(VncState *vs)
{
long ret;
uint8_t encoded[4096];
const char *decoded;
unsigned int decodedLen;
int err;
ret = vnc_client_read_buf(vs, encoded, sizeof(encoded));
if (!ret)
return 0;
err = sasl_decode(vs->sasl.conn,
(char *)encoded, ret,
&decoded, &decodedLen);
if (err != SASL_OK)
return vnc_client_io_error(vs, -1, -EIO);
VNC_DEBUG("Read SASL Encoded %p size %ld Decoded %p size %d\n",
encoded, ret, decoded, decodedLen);
buffer_reserve(&vs->input, decodedLen);
buffer_append(&vs->input, decoded, decodedLen);
return decodedLen;
}
static int vnc_auth_sasl_check_access(VncState *vs)
{
const void *val;
int err;
err = sasl_getprop(vs->sasl.conn, SASL_USERNAME, &val);
if (err != SASL_OK) {
VNC_DEBUG("cannot query SASL username on connection %d (%s)\n",
err, sasl_errstring(err, NULL, NULL));
return -1;
}
if (val == NULL) {
VNC_DEBUG("no client username was found\n");
return -1;
}
VNC_DEBUG("SASL client username %s\n", (const char *)val);
vs->sasl.username = qemu_strdup((const char*)val);
return 0;
}
static int vnc_auth_sasl_check_ssf(VncState *vs)
{
const void *val;
int err, ssf;
if (!vs->sasl.wantSSF)
return 1;
err = sasl_getprop(vs->sasl.conn, SASL_SSF, &val);
if (err != SASL_OK)
return 0;
ssf = *(const int *)val;
VNC_DEBUG("negotiated an SSF of %d\n", ssf);
if (ssf < 56)
return 0; /* 56 is good for Kerberos */
/* Only setup for read initially, because we're about to send an RPC
* reply which must be in plain text. When the next incoming RPC
* arrives, we'll switch on writes too
*
* cf qemudClientReadSASL in qemud.c
*/
vs->sasl.runSSF = 1;
/* We have a SSF that's good enough */
return 1;
}
/*
* Step Msg
*
* Input from client:
*
* u32 clientin-length
* u8-array clientin-string
*
* Output to client:
*
* u32 serverout-length
* u8-array serverout-strin
* u8 continue
*/
static int protocol_client_auth_sasl_step_len(VncState *vs, uint8_t *data, size_t len);
static int protocol_client_auth_sasl_step(VncState *vs, uint8_t *data, size_t len)
{
uint32_t datalen = len;
const char *serverout;
unsigned int serveroutlen;
int err;
char *clientdata = NULL;
/* NB, distinction of NULL vs "" is *critical* in SASL */
if (datalen) {
clientdata = (char*)data;
clientdata[datalen-1] = '\0'; /* Wire includes '\0', but make sure */
datalen--; /* Don't count NULL byte when passing to _start() */
}
VNC_DEBUG("Step using SASL Data %p (%d bytes)\n",
clientdata, datalen);
err = sasl_server_step(vs->sasl.conn,
clientdata,
datalen,
&serverout,
&serveroutlen);
if (err != SASL_OK &&
err != SASL_CONTINUE) {
VNC_DEBUG("sasl step failed %d (%s)\n",
err, sasl_errdetail(vs->sasl.conn));
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
if (serveroutlen > SASL_DATA_MAX_LEN) {
VNC_DEBUG("sasl step reply data too long %d\n",
serveroutlen);
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
VNC_DEBUG("SASL return data %d bytes, nil; %d\n",
serveroutlen, serverout ? 0 : 1);
if (serveroutlen) {
vnc_write_u32(vs, serveroutlen + 1);
vnc_write(vs, serverout, serveroutlen + 1);
} else {
vnc_write_u32(vs, 0);
}
/* Whether auth is complete */
vnc_write_u8(vs, err == SASL_CONTINUE ? 0 : 1);
if (err == SASL_CONTINUE) {
VNC_DEBUG("%s", "Authentication must continue\n");
/* Wait for step length */
vnc_read_when(vs, protocol_client_auth_sasl_step_len, 4);
} else {
if (!vnc_auth_sasl_check_ssf(vs)) {
VNC_DEBUG("Authentication rejected for weak SSF %d\n", vs->csock);
goto authreject;
}
/* Check username whitelist ACL */
if (vnc_auth_sasl_check_access(vs) < 0) {
VNC_DEBUG("Authentication rejected for ACL %d\n", vs->csock);
goto authreject;
}
VNC_DEBUG("Authentication successful %d\n", vs->csock);
vnc_write_u32(vs, 0); /* Accept auth */
/*
* Delay writing in SSF encoded mode until pending output
* buffer is written
*/
if (vs->sasl.runSSF)
vs->sasl.waitWriteSSF = vs->output.offset;
start_client_init(vs);
}
return 0;
authreject:
vnc_write_u32(vs, 1); /* Reject auth */
vnc_write_u32(vs, sizeof("Authentication failed"));
vnc_write(vs, "Authentication failed", sizeof("Authentication failed"));
vnc_flush(vs);
vnc_client_error(vs);
return -1;
authabort:
vnc_client_error(vs);
return -1;
}
static int protocol_client_auth_sasl_step_len(VncState *vs, uint8_t *data, size_t len)
{
uint32_t steplen = read_u32(data, 0);
VNC_DEBUG("Got client step len %d\n", steplen);
if (steplen > SASL_DATA_MAX_LEN) {
VNC_DEBUG("Too much SASL data %d\n", steplen);
vnc_client_error(vs);
return -1;
}
if (steplen == 0)
return protocol_client_auth_sasl_step(vs, NULL, 0);
else
vnc_read_when(vs, protocol_client_auth_sasl_step, steplen);
return 0;
}
/*
* Start Msg
*
* Input from client:
*
* u32 clientin-length
* u8-array clientin-string
*
* Output to client:
*
* u32 serverout-length
* u8-array serverout-strin
* u8 continue
*/
#define SASL_DATA_MAX_LEN (1024 * 1024)
static int protocol_client_auth_sasl_start(VncState *vs, uint8_t *data, size_t len)
{
uint32_t datalen = len;
const char *serverout;
unsigned int serveroutlen;
int err;
char *clientdata = NULL;
/* NB, distinction of NULL vs "" is *critical* in SASL */
if (datalen) {
clientdata = (char*)data;
clientdata[datalen-1] = '\0'; /* Should be on wire, but make sure */
datalen--; /* Don't count NULL byte when passing to _start() */
}
VNC_DEBUG("Start SASL auth with mechanism %s. Data %p (%d bytes)\n",
vs->sasl.mechlist, clientdata, datalen);
err = sasl_server_start(vs->sasl.conn,
vs->sasl.mechlist,
clientdata,
datalen,
&serverout,
&serveroutlen);
if (err != SASL_OK &&
err != SASL_CONTINUE) {
VNC_DEBUG("sasl start failed %d (%s)\n",
err, sasl_errdetail(vs->sasl.conn));
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
if (serveroutlen > SASL_DATA_MAX_LEN) {
VNC_DEBUG("sasl start reply data too long %d\n",
serveroutlen);
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
VNC_DEBUG("SASL return data %d bytes, nil; %d\n",
serveroutlen, serverout ? 0 : 1);
if (serveroutlen) {
vnc_write_u32(vs, serveroutlen + 1);
vnc_write(vs, serverout, serveroutlen + 1);
} else {
vnc_write_u32(vs, 0);
}
/* Whether auth is complete */
vnc_write_u8(vs, err == SASL_CONTINUE ? 0 : 1);
if (err == SASL_CONTINUE) {
VNC_DEBUG("%s", "Authentication must continue\n");
/* Wait for step length */
vnc_read_when(vs, protocol_client_auth_sasl_step_len, 4);
} else {
if (!vnc_auth_sasl_check_ssf(vs)) {
VNC_DEBUG("Authentication rejected for weak SSF %d\n", vs->csock);
goto authreject;
}
/* Check username whitelist ACL */
if (vnc_auth_sasl_check_access(vs) < 0) {
VNC_DEBUG("Authentication rejected for ACL %d\n", vs->csock);
goto authreject;
}
VNC_DEBUG("Authentication successful %d\n", vs->csock);
vnc_write_u32(vs, 0); /* Accept auth */
start_client_init(vs);
}
return 0;
authreject:
vnc_write_u32(vs, 1); /* Reject auth */
vnc_write_u32(vs, sizeof("Authentication failed"));
vnc_write(vs, "Authentication failed", sizeof("Authentication failed"));
vnc_flush(vs);
vnc_client_error(vs);
return -1;
authabort:
vnc_client_error(vs);
return -1;
}
static int protocol_client_auth_sasl_start_len(VncState *vs, uint8_t *data, size_t len)
{
uint32_t startlen = read_u32(data, 0);
VNC_DEBUG("Got client start len %d\n", startlen);
if (startlen > SASL_DATA_MAX_LEN) {
VNC_DEBUG("Too much SASL data %d\n", startlen);
vnc_client_error(vs);
return -1;
}
if (startlen == 0)
return protocol_client_auth_sasl_start(vs, NULL, 0);
vnc_read_when(vs, protocol_client_auth_sasl_start, startlen);
return 0;
}
static int protocol_client_auth_sasl_mechname(VncState *vs, uint8_t *data, size_t len)
{
char *mechname = malloc(len + 1);
if (!mechname) {
VNC_DEBUG("Out of memory reading mechname\n");
vnc_client_error(vs);
}
strncpy(mechname, (char*)data, len);
mechname[len] = '\0';
VNC_DEBUG("Got client mechname '%s' check against '%s'\n",
mechname, vs->sasl.mechlist);
if (strncmp(vs->sasl.mechlist, mechname, len) == 0) {
if (vs->sasl.mechlist[len] != '\0' &&
vs->sasl.mechlist[len] != ',') {
VNC_DEBUG("One %d", vs->sasl.mechlist[len]);
vnc_client_error(vs);
return -1;
}
} else {
char *offset = strstr(vs->sasl.mechlist, mechname);
VNC_DEBUG("Two %p\n", offset);
if (!offset) {
vnc_client_error(vs);
return -1;
}
VNC_DEBUG("Two '%s'\n", offset);
if (offset[-1] != ',' ||
(offset[len] != '\0'&&
offset[len] != ',')) {
vnc_client_error(vs);
return -1;
}
}
free(vs->sasl.mechlist);
vs->sasl.mechlist = mechname;
VNC_DEBUG("Validated mechname '%s'\n", mechname);
vnc_read_when(vs, protocol_client_auth_sasl_start_len, 4);
return 0;
}
static int protocol_client_auth_sasl_mechname_len(VncState *vs, uint8_t *data, size_t len)
{
uint32_t mechlen = read_u32(data, 0);
VNC_DEBUG("Got client mechname len %d\n", mechlen);
if (mechlen > 100) {
VNC_DEBUG("Too long SASL mechname data %d\n", mechlen);
vnc_client_error(vs);
return -1;
}
if (mechlen < 1) {
VNC_DEBUG("Too short SASL mechname %d\n", mechlen);
vnc_client_error(vs);
return -1;
}
vnc_read_when(vs, protocol_client_auth_sasl_mechname,mechlen);
return 0;
}
#define USES_X509_AUTH(vs) \
((vs)->subauth == VNC_AUTH_VENCRYPT_X509NONE || \
(vs)->subauth == VNC_AUTH_VENCRYPT_X509VNC || \
(vs)->subauth == VNC_AUTH_VENCRYPT_X509PLAIN || \
(vs)->subauth == VNC_AUTH_VENCRYPT_X509SASL)
void start_auth_sasl(VncState *vs)
{
const char *mechlist = NULL;
sasl_security_properties_t secprops;
int err;
char *localAddr, *remoteAddr;
int mechlistlen;
VNC_DEBUG("Initialize SASL auth %d\n", vs->csock);
/* Get local & remote client addresses in form IPADDR;PORT */
if (!(localAddr = vnc_socket_local_addr("%s;%s", vs->csock)))
goto authabort;
if (!(remoteAddr = vnc_socket_remote_addr("%s;%s", vs->csock))) {
free(localAddr);
goto authabort;
}
err = sasl_server_new("vnc",
NULL, /* FQDN - just delegates to gethostname */
NULL, /* User realm */
localAddr,
remoteAddr,
NULL, /* Callbacks, not needed */
SASL_SUCCESS_DATA,
&vs->sasl.conn);
free(localAddr);
free(remoteAddr);
localAddr = remoteAddr = NULL;
if (err != SASL_OK) {
VNC_DEBUG("sasl context setup failed %d (%s)",
err, sasl_errstring(err, NULL, NULL));
vs->sasl.conn = NULL;
goto authabort;
}
#ifdef CONFIG_VNC_TLS
/* Inform SASL that we've got an external SSF layer from TLS/x509 */
if (vs->vd->auth == VNC_AUTH_VENCRYPT &&
vs->vd->subauth == VNC_AUTH_VENCRYPT_X509SASL) {
gnutls_cipher_algorithm_t cipher;
sasl_ssf_t ssf;
cipher = gnutls_cipher_get(vs->tls.session);
if (!(ssf = (sasl_ssf_t)gnutls_cipher_get_key_size(cipher))) {
VNC_DEBUG("%s", "cannot TLS get cipher size\n");
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
ssf *= 8; /* tls key size is bytes, sasl wants bits */
err = sasl_setprop(vs->sasl.conn, SASL_SSF_EXTERNAL, &ssf);
if (err != SASL_OK) {
VNC_DEBUG("cannot set SASL external SSF %d (%s)\n",
err, sasl_errstring(err, NULL, NULL));
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
} else
#endif /* CONFIG_VNC_TLS */
vs->sasl.wantSSF = 1;
memset (&secprops, 0, sizeof secprops);
/* Inform SASL that we've got an external SSF layer from TLS */
if (strncmp(vs->vd->display, "unix:", 5) == 0
#ifdef CONFIG_VNC_TLS
/* Disable SSF, if using TLS+x509+SASL only. TLS without x509
is not sufficiently strong */
|| (vs->vd->auth == VNC_AUTH_VENCRYPT &&
vs->vd->subauth == VNC_AUTH_VENCRYPT_X509SASL)
#endif /* CONFIG_VNC_TLS */
) {
/* If we've got TLS or UNIX domain sock, we don't care about SSF */
secprops.min_ssf = 0;
secprops.max_ssf = 0;
secprops.maxbufsize = 8192;
secprops.security_flags = 0;
} else {
/* Plain TCP, better get an SSF layer */
secprops.min_ssf = 56; /* Good enough to require kerberos */
secprops.max_ssf = 100000; /* Arbitrary big number */
secprops.maxbufsize = 8192;
/* Forbid any anonymous or trivially crackable auth */
secprops.security_flags =
SASL_SEC_NOANONYMOUS | SASL_SEC_NOPLAINTEXT;
}
err = sasl_setprop(vs->sasl.conn, SASL_SEC_PROPS, &secprops);
if (err != SASL_OK) {
VNC_DEBUG("cannot set SASL security props %d (%s)\n",
err, sasl_errstring(err, NULL, NULL));
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
err = sasl_listmech(vs->sasl.conn,
NULL, /* Don't need to set user */
"", /* Prefix */
",", /* Separator */
"", /* Suffix */
&mechlist,
NULL,
NULL);
if (err != SASL_OK) {
VNC_DEBUG("cannot list SASL mechanisms %d (%s)\n",
err, sasl_errdetail(vs->sasl.conn));
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
VNC_DEBUG("Available mechanisms for client: '%s'\n", mechlist);
if (!(vs->sasl.mechlist = strdup(mechlist))) {
VNC_DEBUG("Out of memory");
sasl_dispose(&vs->sasl.conn);
vs->sasl.conn = NULL;
goto authabort;
}
mechlistlen = strlen(mechlist);
vnc_write_u32(vs, mechlistlen);
vnc_write(vs, mechlist, mechlistlen);
vnc_flush(vs);
VNC_DEBUG("Wait for client mechname length\n");
vnc_read_when(vs, protocol_client_auth_sasl_mechname_len, 4);
return;
authabort:
vnc_client_error(vs);
return;
}