char: introduce support for TLS encrypted TCP chardev backend

This integrates support for QIOChannelTLS object in the TCP
chardev backend. If the 'tls-creds=NAME' option is passed with
the '-chardev tcp' argument, then it will setup the chardev
such that the client is required to establish a TLS handshake
when connecting. There is no support for checking the client
certificate against ACLs in this initial patch. This is pending
work to QOM-ify the ACL object code.

A complete invocation to run QEMU as the server for a TLS
encrypted serial dev might be

  $ qemu-system-x86_64 \
      -nodefconfig -nodefaults -device sga -display none \
      -chardev socket,id=s0,host=127.0.0.1,port=9000,tls-creds=tls0,server \
      -device isa-serial,chardev=s0 \
      -object tls-creds-x509,id=tls0,endpoint=server,verify-peer=off,\
         dir=/home/berrange/security/qemutls

To test with the gnutls-cli tool as the client:

  $ gnutls-cli --priority=NORMAL -p 9000 \
       --x509cafile=/home/berrange/security/qemutls/ca-cert.pem \
       127.0.0.1

If QEMU was told to use 'anon' credential type, then use the
priority string 'NORMAL:+ANON-DH' with gnutls-cli

Alternatively, if setting up a chardev to operate as a client,
then the TLS credentials registered must be for the client
endpoint. First a TLS server must be setup, which can be done
with the gnutls-serv tool

  $ gnutls-serv --priority=NORMAL -p 9000 --echo \
       --x509cafile=/home/berrange/security/qemutls/ca-cert.pem \
       --x509certfile=/home/berrange/security/qemutls/server-cert.pem \
       --x509keyfile=/home/berrange/security/qemutls/server-key.pem

Then QEMU can connect with

  $ qemu-system-x86_64 \
      -nodefconfig -nodefaults -device sga -display none \
      -chardev socket,id=s0,host=127.0.0.1,port=9000,tls-creds=tls0 \
      -device isa-serial,chardev=s0 \
      -object tls-creds-x509,id=tls0,endpoint=client,\
        dir=/home/berrange/security/qemutls

Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
Message-Id: <1453202071-10289-5-git-send-email-berrange@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
This commit is contained in:
Daniel P. Berrange 2016-01-19 11:14:31 +00:00 committed by Paolo Bonzini
parent f2001a7e05
commit a8fb542705
3 changed files with 134 additions and 13 deletions

View file

@ -3146,6 +3146,7 @@
# #
# @addr: socket address to listen on (server=true) # @addr: socket address to listen on (server=true)
# or connect to (server=false) # or connect to (server=false)
# @tls-creds: #optional the ID of the TLS credentials object (since 2.6)
# @server: #optional create server socket (default: true) # @server: #optional create server socket (default: true)
# @wait: #optional wait for incoming connection on server # @wait: #optional wait for incoming connection on server
# sockets (default: false). # sockets (default: false).
@ -3160,6 +3161,7 @@
# Since: 1.4 # Since: 1.4
## ##
{ 'struct': 'ChardevSocket', 'data': { 'addr' : 'SocketAddress', { 'struct': 'ChardevSocket', 'data': { 'addr' : 'SocketAddress',
'*tls-creds' : 'str',
'*server' : 'bool', '*server' : 'bool',
'*wait' : 'bool', '*wait' : 'bool',
'*nodelay' : 'bool', '*nodelay' : 'bool',

View file

@ -35,6 +35,7 @@
#include "qemu/base64.h" #include "qemu/base64.h"
#include "io/channel-socket.h" #include "io/channel-socket.h"
#include "io/channel-file.h" #include "io/channel-file.h"
#include "io/channel-tls.h"
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
@ -2532,9 +2533,11 @@ static CharDriverState *qemu_chr_open_udp(QIOChannelSocket *sioc,
/* TCP Net console */ /* TCP Net console */
typedef struct { typedef struct {
QIOChannel *ioc; QIOChannel *ioc; /* Client I/O channel */
QIOChannelSocket *sioc; /* Client master channel */
QIOChannelSocket *listen_ioc; QIOChannelSocket *listen_ioc;
guint listen_tag; guint listen_tag;
QCryptoTLSCreds *tls_creds;
int connected; int connected;
int max_size; int max_size;
int do_telnetopt; int do_telnetopt;
@ -2776,6 +2779,8 @@ static void tcp_chr_disconnect(CharDriverState *chr)
QIO_CHANNEL(s->listen_ioc), G_IO_IN, tcp_chr_accept, chr, NULL); QIO_CHANNEL(s->listen_ioc), G_IO_IN, tcp_chr_accept, chr, NULL);
} }
remove_fd_in_watch(chr); remove_fd_in_watch(chr);
object_unref(OBJECT(s->sioc));
s->sioc = NULL;
object_unref(OBJECT(s->ioc)); object_unref(OBJECT(s->ioc));
s->ioc = NULL; s->ioc = NULL;
g_free(chr->filename); g_free(chr->filename);
@ -2849,12 +2854,12 @@ static void tcp_chr_connect(void *opaque)
{ {
CharDriverState *chr = opaque; CharDriverState *chr = opaque;
TCPCharDriver *s = chr->opaque; TCPCharDriver *s = chr->opaque;
QIOChannelSocket *sioc = QIO_CHANNEL_SOCKET(s->ioc);
g_free(chr->filename); g_free(chr->filename);
chr->filename = sockaddr_to_str(&sioc->localAddr, sioc->localAddrLen, chr->filename = sockaddr_to_str(
&sioc->remoteAddr, sioc->remoteAddrLen, &s->sioc->localAddr, s->sioc->localAddrLen,
s->is_listen, s->is_telnet); &s->sioc->remoteAddr, s->sioc->remoteAddrLen,
s->is_listen, s->is_telnet);
s->connected = 1; s->connected = 1;
if (s->ioc) { if (s->ioc) {
@ -2943,6 +2948,57 @@ static void tcp_chr_telnet_init(CharDriverState *chr)
init, NULL); init, NULL);
} }
static void tcp_chr_tls_handshake(Object *source,
Error *err,
gpointer user_data)
{
CharDriverState *chr = user_data;
TCPCharDriver *s = chr->opaque;
if (err) {
tcp_chr_disconnect(chr);
} else {
if (s->do_telnetopt) {
tcp_chr_telnet_init(chr);
} else {
tcp_chr_connect(chr);
}
}
}
static void tcp_chr_tls_init(CharDriverState *chr)
{
TCPCharDriver *s = chr->opaque;
QIOChannelTLS *tioc;
Error *err = NULL;
if (s->is_listen) {
tioc = qio_channel_tls_new_server(
s->ioc, s->tls_creds,
NULL, /* XXX Use an ACL */
&err);
} else {
tioc = qio_channel_tls_new_client(
s->ioc, s->tls_creds,
s->addr->u.inet->host,
&err);
}
if (tioc == NULL) {
error_free(err);
tcp_chr_disconnect(chr);
}
object_unref(OBJECT(s->ioc));
s->ioc = QIO_CHANNEL(tioc);
qio_channel_tls_handshake(tioc,
tcp_chr_tls_handshake,
chr,
NULL);
}
static int tcp_chr_new_client(CharDriverState *chr, QIOChannelSocket *sioc) static int tcp_chr_new_client(CharDriverState *chr, QIOChannelSocket *sioc)
{ {
TCPCharDriver *s = chr->opaque; TCPCharDriver *s = chr->opaque;
@ -2952,6 +3008,8 @@ static int tcp_chr_new_client(CharDriverState *chr, QIOChannelSocket *sioc)
s->ioc = QIO_CHANNEL(sioc); s->ioc = QIO_CHANNEL(sioc);
object_ref(OBJECT(sioc)); object_ref(OBJECT(sioc));
s->sioc = sioc;
object_ref(OBJECT(sioc));
if (s->do_nodelay) { if (s->do_nodelay) {
qio_channel_set_delay(s->ioc, false); qio_channel_set_delay(s->ioc, false);
@ -2961,10 +3019,14 @@ static int tcp_chr_new_client(CharDriverState *chr, QIOChannelSocket *sioc)
s->listen_tag = 0; s->listen_tag = 0;
} }
if (s->do_telnetopt) { if (s->tls_creds) {
tcp_chr_telnet_init(chr); tcp_chr_tls_init(chr);
} else { } else {
tcp_chr_connect(chr); if (s->do_telnetopt) {
tcp_chr_telnet_init(chr);
} else {
tcp_chr_connect(chr);
}
} }
return 0; return 0;
@ -3033,6 +3095,9 @@ static void tcp_chr_close(CharDriverState *chr)
} }
g_free(s->read_msgfds); g_free(s->read_msgfds);
} }
if (s->tls_creds) {
object_unref(OBJECT(s->tls_creds));
}
if (s->write_msgfds_num) { if (s->write_msgfds_num) {
g_free(s->write_msgfds); g_free(s->write_msgfds);
} }
@ -3563,6 +3628,7 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend,
const char *path = qemu_opt_get(opts, "path"); const char *path = qemu_opt_get(opts, "path");
const char *host = qemu_opt_get(opts, "host"); const char *host = qemu_opt_get(opts, "host");
const char *port = qemu_opt_get(opts, "port"); const char *port = qemu_opt_get(opts, "port");
const char *tls_creds = qemu_opt_get(opts, "tls-creds");
SocketAddress *addr; SocketAddress *addr;
if (!path) { if (!path) {
@ -3574,6 +3640,11 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend,
error_setg(errp, "chardev: socket: no port given"); error_setg(errp, "chardev: socket: no port given");
return; return;
} }
} else {
if (tls_creds) {
error_setg(errp, "TLS can only be used over TCP socket");
return;
}
} }
backend->u.socket = g_new0(ChardevSocket, 1); backend->u.socket = g_new0(ChardevSocket, 1);
@ -3589,6 +3660,7 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend,
backend->u.socket->wait = is_waitconnect; backend->u.socket->wait = is_waitconnect;
backend->u.socket->has_reconnect = true; backend->u.socket->has_reconnect = true;
backend->u.socket->reconnect = reconnect; backend->u.socket->reconnect = reconnect;
backend->u.socket->tls_creds = g_strdup(tls_creds);
addr = g_new0(SocketAddress, 1); addr = g_new0(SocketAddress, 1);
if (path) { if (path) {
@ -4015,6 +4087,9 @@ QemuOptsList qemu_chardev_opts = {
},{ },{
.name = "telnet", .name = "telnet",
.type = QEMU_OPT_BOOL, .type = QEMU_OPT_BOOL,
},{
.name = "tls-creds",
.type = QEMU_OPT_STRING,
},{ },{
.name = "width", .name = "width",
.type = QEMU_OPT_NUMBER, .type = QEMU_OPT_NUMBER,
@ -4231,6 +4306,39 @@ static CharDriverState *qmp_chardev_open_socket(const char *id,
s->is_listen = is_listen; s->is_listen = is_listen;
s->is_telnet = is_telnet; s->is_telnet = is_telnet;
s->do_nodelay = do_nodelay; s->do_nodelay = do_nodelay;
if (sock->tls_creds) {
Object *creds;
creds = object_resolve_path_component(
object_get_objects_root(), sock->tls_creds);
if (!creds) {
error_setg(errp, "No TLS credentials with id '%s'",
sock->tls_creds);
goto error;
}
s->tls_creds = (QCryptoTLSCreds *)
object_dynamic_cast(creds,
TYPE_QCRYPTO_TLS_CREDS);
if (!s->tls_creds) {
error_setg(errp, "Object with id '%s' is not TLS credentials",
sock->tls_creds);
goto error;
}
object_ref(OBJECT(s->tls_creds));
if (is_listen) {
if (s->tls_creds->endpoint != QCRYPTO_TLS_CREDS_ENDPOINT_SERVER) {
error_setg(errp, "%s",
"Expected TLS credentials for server endpoint");
goto error;
}
} else {
if (s->tls_creds->endpoint != QCRYPTO_TLS_CREDS_ENDPOINT_CLIENT) {
error_setg(errp, "%s",
"Expected TLS credentials for client endpoint");
goto error;
}
}
}
qapi_copy_SocketAddress(&s->addr, sock->addr); qapi_copy_SocketAddress(&s->addr, sock->addr);
chr->opaque = s; chr->opaque = s;
@ -4259,9 +4367,7 @@ static CharDriverState *qmp_chardev_open_socket(const char *id,
if (s->reconnect_time) { if (s->reconnect_time) {
socket_try_connect(chr); socket_try_connect(chr);
} else if (!qemu_chr_open_socket_fd(chr, errp)) { } else if (!qemu_chr_open_socket_fd(chr, errp)) {
g_free(s); goto error;
qemu_chr_free_common(chr);
return NULL;
} }
if (is_listen && is_waitconnect) { if (is_listen && is_waitconnect) {
@ -4272,6 +4378,14 @@ static CharDriverState *qmp_chardev_open_socket(const char *id,
} }
return chr; return chr;
error:
if (s->tls_creds) {
object_unref(OBJECT(s->tls_creds));
}
g_free(s);
qemu_chr_free_common(chr);
return NULL;
} }
static CharDriverState *qmp_chardev_open_udp(const char *id, static CharDriverState *qmp_chardev_open_udp(const char *id,

View file

@ -2092,7 +2092,7 @@ DEF("chardev", HAS_ARG, QEMU_OPTION_chardev,
"-chardev null,id=id[,mux=on|off][,logfile=PATH][,logappend=on|off]\n" "-chardev null,id=id[,mux=on|off][,logfile=PATH][,logappend=on|off]\n"
"-chardev socket,id=id[,host=host],port=port[,to=to][,ipv4][,ipv6][,nodelay][,reconnect=seconds]\n" "-chardev socket,id=id[,host=host],port=port[,to=to][,ipv4][,ipv6][,nodelay][,reconnect=seconds]\n"
" [,server][,nowait][,telnet][,reconnect=seconds][,mux=on|off]\n" " [,server][,nowait][,telnet][,reconnect=seconds][,mux=on|off]\n"
" [,logfile=PATH][,logappend=on|off] (tcp)\n" " [,logfile=PATH][,logappend=on|off][,tls-creds=ID] (tcp)\n"
"-chardev socket,id=id,path=path[,server][,nowait][,telnet][,reconnect=seconds]\n" "-chardev socket,id=id,path=path[,server][,nowait][,telnet][,reconnect=seconds]\n"
" [,mux=on|off][,logfile=PATH][,logappend=on|off] (unix)\n" " [,mux=on|off][,logfile=PATH][,logappend=on|off] (unix)\n"
"-chardev udp,id=id[,host=host],port=port[,localaddr=localaddr]\n" "-chardev udp,id=id[,host=host],port=port[,localaddr=localaddr]\n"
@ -2172,7 +2172,7 @@ Further options to each backend are described below.
A void device. This device will not emit any data, and will drop any data it A void device. This device will not emit any data, and will drop any data it
receives. The null backend does not take any options. receives. The null backend does not take any options.
@item -chardev socket ,id=@var{id} [@var{TCP options} or @var{unix options}] [,server] [,nowait] [,telnet] [,reconnect=@var{seconds}] @item -chardev socket ,id=@var{id} [@var{TCP options} or @var{unix options}] [,server] [,nowait] [,telnet] [,reconnect=@var{seconds}] [,tls-creds=@var{id}]
Create a two-way stream socket, which can be either a TCP or a unix socket. A Create a two-way stream socket, which can be either a TCP or a unix socket. A
unix socket will be created if @option{path} is specified. Behaviour is unix socket will be created if @option{path} is specified. Behaviour is
@ -2190,6 +2190,11 @@ escape sequences.
the remote end goes away. qemu will delay this many seconds and then attempt the remote end goes away. qemu will delay this many seconds and then attempt
to reconnect. Zero disables reconnecting, and is the default. to reconnect. Zero disables reconnecting, and is the default.
@option{tls-creds} requests enablement of the TLS protocol for encryption,
and specifies the id of the TLS credentials to use for the handshake. The
credentials must be previously created with the @option{-object tls-creds}
argument.
TCP and unix socket options are given below: TCP and unix socket options are given below:
@table @option @table @option