From patchwork Fri Apr 17 14:22:37 2015 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= X-Patchwork-Id: 462094 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [IPv6:2001:4830:134:3::11]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id CE84D14029E for ; Sat, 18 Apr 2015 00:35:34 +1000 (AEST) Received: from localhost ([::1]:41758 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1Yj7Mi-0003A6-OO for incoming@patchwork.ozlabs.org; Fri, 17 Apr 2015 10:35:32 -0400 Received: from eggs.gnu.org ([2001:4830:134:3::10]:59277) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1Yj7E7-0002FA-MJ for qemu-devel@nongnu.org; Fri, 17 Apr 2015 10:26:41 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1Yj7E4-0004Gg-64 for qemu-devel@nongnu.org; Fri, 17 Apr 2015 10:26:39 -0400 Received: from mx1.redhat.com ([209.132.183.28]:55371) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1Yj7E3-0004Gc-LN for qemu-devel@nongnu.org; Fri, 17 Apr 2015 10:26:35 -0400 Received: from int-mx13.intmail.prod.int.phx2.redhat.com (int-mx13.intmail.prod.int.phx2.redhat.com [10.5.11.26]) by mx1.redhat.com (Postfix) with ESMTPS id 5A36EAB850 for ; Fri, 17 Apr 2015 14:26:35 +0000 (UTC) Received: from localhost.localdomain.com (vpn1-5-19.ams2.redhat.com [10.36.5.19]) by int-mx13.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id t3HENPZh011852; Fri, 17 Apr 2015 10:26:29 -0400 From: "Daniel P. Berrange" To: qemu-devel@nongnu.org Date: Fri, 17 Apr 2015 15:22:37 +0100 Message-Id: <1429280557-8887-35-git-send-email-berrange@redhat.com> In-Reply-To: <1429280557-8887-1-git-send-email-berrange@redhat.com> References: <1429280557-8887-1-git-send-email-berrange@redhat.com> X-Scanned-By: MIMEDefang 2.68 on 10.5.11.26 X-detected-operating-system: by eggs.gnu.org: GNU/Linux 3.x X-Received-From: 209.132.183.28 Cc: Paolo Bonzini , Gerd Hoffmann , Stefan Hajnoczi Subject: [Qemu-devel] [PATCH v1 RFC 34/34] char: introduce support for TLS encrypted TCP chardev backend X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org This integrates support for QIOChannelTLS object in the TCP chardev backend. If the 'tls-cred=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. The 'acl' option will further enable the creation of a 'char.$ID.tlspeername' ACL which will be used to validate the client x509 certificate, if provided. A complete invokation 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-cred=tls0,server \ -device isa-serial,chardev=s0 \ -object qcrypto-tls-cred,id=tls0,credtype=x509,\ endpoint=server,dir=/home/berrange/security/qemutls,verify-peer=off 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 'NOMAL:+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 \ --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-cred=tls0 \ -device isa-serial,chardev=s0 \ -object qcrypto-tls-cred,id=tls0,credtype=x509,\ endpoint=client,dir=/home/berrange/security/qemutls Signed-off-by: Daniel P. Berrange --- qapi-schema.json | 2 + qemu-char.c | 182 ++++++++++++++++++++++++++++++++++++++++++++++--------- qemu-options.hx | 9 ++- 3 files changed, 161 insertions(+), 32 deletions(-) diff --git a/qapi-schema.json b/qapi-schema.json index ac9594d..062a455 100644 --- a/qapi-schema.json +++ b/qapi-schema.json @@ -2782,6 +2782,8 @@ # Since: 1.4 ## { 'type': 'ChardevSocket', 'data': { 'addr' : 'SocketAddress', + '*tls-cred' : 'str', + '*acl' : 'str', '*server' : 'bool', '*wait' : 'bool', '*nodelay' : 'bool', diff --git a/qemu-char.c b/qemu-char.c index 85fbbaf..da6f188 100644 --- a/qemu-char.c +++ b/qemu-char.c @@ -33,6 +33,8 @@ #include "qapi-visit.h" #include "io/channel-socket.h" #include "io/channel-file.h" +#include "io/channel-tls.h" +#include "qemu/acl.h" #include #include @@ -2466,9 +2468,12 @@ static CharDriverState *qemu_chr_open_udp_fd(int fd) /* TCP Net console */ typedef struct { - QIOChannel *ioc; + QIOChannel *ioc; /* Client I/O channel */ + QIOChannelSocket *sioc; /* Client master channel */ QIOChannelSocket *listen_ioc; guint listen_tag; + QCryptoTLSCreds *tls_creds; + char *acl; int connected; int max_size; int do_telnetopt; @@ -2699,6 +2704,8 @@ static void tcp_chr_disconnect(CharDriverState *chr) QIO_CHANNEL(s->listen_ioc), G_IO_IN, tcp_chr_accept, chr); } remove_fd_in_watch(chr); + object_unref(OBJECT(s->sioc)); + s->sioc = NULL; object_unref(OBJECT(s->ioc)); s->ioc = NULL; SocketAddress_to_str(chr->filename, CHR_MAX_FILENAME_SIZE, @@ -2777,11 +2784,10 @@ static void tcp_chr_connect(void *opaque) { CharDriverState *chr = opaque; TCPCharDriver *s = chr->opaque; - QIOChannelSocket *sioc = QIO_CHANNEL_SOCKET(s->ioc); sockaddr_to_str(chr->filename, CHR_MAX_FILENAME_SIZE, - &sioc->localAddr, sioc->localAddrLen, - &sioc->remoteAddr, sioc->remoteAddrLen, + &s->sioc->localAddr, s->sioc->localAddrLen, + &s->sioc->remoteAddr, s->sioc->remoteAddrLen, s->is_listen, s->is_telnet); s->connected = 1; @@ -2869,29 +2875,85 @@ static void tcp_chr_telnet_init(CharDriverState *chr) init); } -static int tcp_chr_new_client(CharDriverState *chr, QIOChannel *ioc) + +static void tcp_chr_tls_handshake(QIOTask *task, + gpointer user_data) +{ + CharDriverState *chr = user_data; + TCPCharDriver *s = chr->opaque; + + if (task->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, + s->acl, + &err); + } else { + tioc = qio_channel_tls_new_client( + s->ioc, s->tls_creds, + s->addr->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) { TCPCharDriver *s = chr->opaque; if (s->ioc != NULL) { return -1; } - s->ioc = ioc; - object_ref(OBJECT(ioc)); + s->ioc = QIO_CHANNEL(sioc); + object_ref(OBJECT(s->ioc)); + s->sioc = sioc; + object_ref(OBJECT(s->sioc)); qio_channel_set_blocking(s->ioc, false); if (s->do_nodelay) { - qio_channel_socket_set_nodelay(QIO_CHANNEL_SOCKET(s->ioc), true); + qio_channel_socket_set_nodelay(s->sioc, true); } if (s->listen_tag) { g_source_remove(s->listen_tag); s->listen_tag = 0; } - if (s->do_telnetopt) { - tcp_chr_telnet_init(chr); + if (s->tls_creds) { + tcp_chr_tls_init(chr); } else { - tcp_chr_connect(chr); + if (s->do_telnetopt) { + tcp_chr_telnet_init(chr); + } else { + tcp_chr_connect(chr); + } } return 0; @@ -2901,14 +2963,14 @@ static int tcp_chr_new_client(CharDriverState *chr, QIOChannel *ioc) static int tcp_chr_add_client(CharDriverState *chr, int fd) { int ret; - QIOChannel *ioc; + QIOChannelSocket *sioc; - ioc = QIO_CHANNEL(qio_channel_socket_new_fd(fd, NULL)); - if (!ioc) { + sioc = qio_channel_socket_new_fd(fd, NULL); + if (!sioc) { return -1; } - ret = tcp_chr_new_client(chr, ioc); - object_unref(OBJECT(ioc)); + ret = tcp_chr_new_client(chr, sioc); + object_unref(OBJECT(sioc)); return ret; } @@ -2917,17 +2979,16 @@ static gboolean tcp_chr_accept(QIOChannel *channel, void *opaque) { CharDriverState *chr = opaque; - QIOChannel *ioc; + QIOChannelSocket *sioc; - ioc = QIO_CHANNEL( - qio_channel_socket_accept(QIO_CHANNEL_SOCKET(channel), NULL)); - if (!ioc) { + sioc = qio_channel_socket_accept(QIO_CHANNEL_SOCKET(channel), NULL); + if (!sioc) { return TRUE; } - tcp_chr_new_client(chr, ioc); + tcp_chr_new_client(chr, sioc); - object_unref(OBJECT(ioc)); + object_unref(OBJECT(sioc)); return TRUE; } @@ -2959,6 +3020,9 @@ static void tcp_chr_close(CharDriverState *chr) } g_free(s->read_msgfds); } + if (s->tls_creds) { + object_unref(OBJECT(s->tls_creds)); + } if (s->write_msgfds_num) { g_free(s->write_msgfds); } @@ -2979,13 +3043,13 @@ static void qemu_chr_finish_socket_connection(CharDriverState *chr, int fd) s->listen_tag = qio_channel_add_watch( QIO_CHANNEL(s->listen_ioc), G_IO_IN, tcp_chr_accept, chr); } else { - QIOChannel *ioc = QIO_CHANNEL(qio_channel_socket_new_fd(fd, NULL)); - if (!ioc) { + QIOChannelSocket *sioc = qio_channel_socket_new_fd(fd, NULL); + if (!sioc) { close(fd); return; } - tcp_chr_new_client(chr, ioc); - object_unref(OBJECT(ioc)); + tcp_chr_new_client(chr, sioc); + object_unref(OBJECT(sioc)); } } @@ -3450,6 +3514,8 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend, const char *path = qemu_opt_get(opts, "path"); const char *host = qemu_opt_get(opts, "host"); const char *port = qemu_opt_get(opts, "port"); + const char *tls_cred = qemu_opt_get(opts, "tls-cred"); + bool acl = qemu_opt_get_bool(opts, "acl", false); SocketAddress *addr; if (!path) { @@ -3461,6 +3527,11 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend, error_setg(errp, "chardev: socket: no port given"); return; } + } else { + if (tls_cred) { + error_setg(errp, "TLS can only be used over TCP socket"); + return; + } } backend->socket = g_new0(ChardevSocket, 1); @@ -3475,6 +3546,11 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend, backend->socket->wait = is_waitconnect; backend->socket->has_reconnect = true; backend->socket->reconnect = reconnect; + backend->socket->tls_cred = g_strdup(tls_cred); + if (acl) { + backend->socket->acl = g_strdup_printf("chr.%s.tlspeername", + qemu_opts_id(opts)); + } addr = g_new0(SocketAddress, 1); if (path) { @@ -3494,6 +3570,7 @@ static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend, addr->inet->ipv6 = qemu_opt_get_bool(opts, "ipv6", 0); } backend->socket->addr = addr; + return; } static void qemu_chr_parse_udp(QemuOpts *opts, ChardevBackend *backend, @@ -3877,6 +3954,9 @@ QemuOptsList qemu_chardev_opts = { .name = "telnet", .type = QEMU_OPT_BOOL, },{ + .name = "tls-cred", + .type = QEMU_OPT_STRING, + },{ .name = "width", .type = QEMU_OPT_NUMBER, },{ @@ -4061,6 +4141,41 @@ static CharDriverState *qmp_chardev_open_socket(ChardevSocket *sock, s->is_listen = is_listen; s->is_telnet = is_telnet; s->do_nodelay = do_nodelay; + if (sock->tls_cred) { + Object *container; + container = container_get(object_get_root(), "/objects"); + /* XXX this type cast causes an abort if the type is wrong. + * This is bad. We should check and set an error */ + s->tls_creds = QCRYPTO_TLS_CREDS( + object_resolve_path_component(container, + sock->tls_cred)); + if (!s->tls_creds) { + error_setg(errp, "No TLS credentials with id '%s'", + sock->tls_cred); + 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; + } + } + + if (sock->acl) { + s->acl = g_strdup(sock->acl); + + qemu_acl_init(s->acl); + } + } + qapi_copy_SocketAddress(&s->addr, sock->addr); chr->opaque = s; @@ -4090,10 +4205,7 @@ static CharDriverState *qmp_chardev_open_socket(ChardevSocket *sock, if (s->reconnect_time) { socket_try_connect(chr); } else if (!qemu_chr_open_socket_fd(chr, errp)) { - g_free(s); - g_free(chr->filename); - g_free(chr); - return NULL; + goto error; } if (is_listen && is_waitconnect) { @@ -4104,6 +4216,16 @@ static CharDriverState *qmp_chardev_open_socket(ChardevSocket *sock, } return chr; + + error: + if (s->tls_creds) { + object_unref(OBJECT(s->tls_creds)); + } + g_free(s->acl); + g_free(s); + g_free(chr->filename); + g_free(chr); + return NULL; } static CharDriverState *qmp_chardev_open_udp(ChardevUdp *udp, diff --git a/qemu-options.hx b/qemu-options.hx index 44d9be2..0c764fc 100644 --- a/qemu-options.hx +++ b/qemu-options.hx @@ -2009,7 +2009,7 @@ ETEXI DEF("chardev", HAS_ARG, QEMU_OPTION_chardev, "-chardev null,id=id[,mux=on|off]\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] (tcp)\n" + " [,server][,nowait][,telnet][,reconnect=seconds][,mux=on|off][,tls-cred=ID][,acl] (tcp)\n" "-chardev socket,id=id,path=path[,server][,nowait][,telnet][,reconnect=seconds][,mux=on|off] (unix)\n" "-chardev udp,id=id[,host=host],port=port[,localaddr=localaddr]\n" " [,localport=localport][,ipv4][,ipv6][,mux=on|off]\n" @@ -2082,7 +2082,7 @@ Options to each backend are described below. 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. -@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-cred=@var{id}] 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 @@ -2100,6 +2100,11 @@ escape sequences. the remote end goes away. qemu will delay this many seconds and then attempt to reconnect. Zero disables reconnecting, and is the default. +@option{tls-cred} 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 qcrypto-tls-cred} +argument. + TCP and unix socket options are given below: @table @option