a1c5e949dd
Modern gnutls can use a global config file to control the crypto priority settings for TLS connections. For example the priority string "@SYSTEM" instructs gnutls to find the priority setting named "SYSTEM" in the global config file. Latest gnutls GIT codebase gained the ability to reference multiple priority strings in the config file, with the first one that is found to existing winning. This means it is now possible to configure QEMU out of the box with a default priority of "@QEMU,SYSTEM", which says to look for the settings "QEMU" first, and if not found, use the "SYSTEM" settings. To make use of this facility, we introduce the ability to set the QEMU default priority at build time via a new configure argument. It is anticipated that distro vendors will set this when building QEMU to a suitable value for use with distro crypto policy setup. eg current Fedora would run ./configure --tls-priority=@SYSTEM while future Fedora would run ./configure --tls-priority=@QEMU,SYSTEM Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
589 lines
16 KiB
C
589 lines
16 KiB
C
/*
|
|
* QEMU crypto TLS session support
|
|
*
|
|
* Copyright (c) 2015 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 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "crypto/tlssession.h"
|
|
#include "crypto/tlscredsanon.h"
|
|
#include "crypto/tlscredsx509.h"
|
|
#include "qapi/error.h"
|
|
#include "qemu/acl.h"
|
|
#include "trace.h"
|
|
|
|
#ifdef CONFIG_GNUTLS
|
|
|
|
|
|
#include <gnutls/x509.h>
|
|
|
|
|
|
struct QCryptoTLSSession {
|
|
QCryptoTLSCreds *creds;
|
|
gnutls_session_t handle;
|
|
char *hostname;
|
|
char *aclname;
|
|
bool handshakeComplete;
|
|
QCryptoTLSSessionWriteFunc writeFunc;
|
|
QCryptoTLSSessionReadFunc readFunc;
|
|
void *opaque;
|
|
char *peername;
|
|
};
|
|
|
|
|
|
void
|
|
qcrypto_tls_session_free(QCryptoTLSSession *session)
|
|
{
|
|
if (!session) {
|
|
return;
|
|
}
|
|
|
|
gnutls_deinit(session->handle);
|
|
g_free(session->hostname);
|
|
g_free(session->peername);
|
|
g_free(session->aclname);
|
|
object_unref(OBJECT(session->creds));
|
|
g_free(session);
|
|
}
|
|
|
|
|
|
static ssize_t
|
|
qcrypto_tls_session_push(void *opaque, const void *buf, size_t len)
|
|
{
|
|
QCryptoTLSSession *session = opaque;
|
|
|
|
if (!session->writeFunc) {
|
|
errno = EIO;
|
|
return -1;
|
|
};
|
|
|
|
return session->writeFunc(buf, len, session->opaque);
|
|
}
|
|
|
|
|
|
static ssize_t
|
|
qcrypto_tls_session_pull(void *opaque, void *buf, size_t len)
|
|
{
|
|
QCryptoTLSSession *session = opaque;
|
|
|
|
if (!session->readFunc) {
|
|
errno = EIO;
|
|
return -1;
|
|
};
|
|
|
|
return session->readFunc(buf, len, session->opaque);
|
|
}
|
|
|
|
|
|
QCryptoTLSSession *
|
|
qcrypto_tls_session_new(QCryptoTLSCreds *creds,
|
|
const char *hostname,
|
|
const char *aclname,
|
|
QCryptoTLSCredsEndpoint endpoint,
|
|
Error **errp)
|
|
{
|
|
QCryptoTLSSession *session;
|
|
int ret;
|
|
|
|
session = g_new0(QCryptoTLSSession, 1);
|
|
trace_qcrypto_tls_session_new(
|
|
session, creds, hostname ? hostname : "<none>",
|
|
aclname ? aclname : "<none>", endpoint);
|
|
|
|
if (hostname) {
|
|
session->hostname = g_strdup(hostname);
|
|
}
|
|
if (aclname) {
|
|
session->aclname = g_strdup(aclname);
|
|
}
|
|
session->creds = creds;
|
|
object_ref(OBJECT(creds));
|
|
|
|
if (creds->endpoint != endpoint) {
|
|
error_setg(errp, "Credentials endpoint doesn't match session");
|
|
goto error;
|
|
}
|
|
|
|
if (endpoint == QCRYPTO_TLS_CREDS_ENDPOINT_SERVER) {
|
|
ret = gnutls_init(&session->handle, GNUTLS_SERVER);
|
|
} else {
|
|
ret = gnutls_init(&session->handle, GNUTLS_CLIENT);
|
|
}
|
|
if (ret < 0) {
|
|
error_setg(errp, "Cannot initialize TLS session: %s",
|
|
gnutls_strerror(ret));
|
|
goto error;
|
|
}
|
|
|
|
if (object_dynamic_cast(OBJECT(creds),
|
|
TYPE_QCRYPTO_TLS_CREDS_ANON)) {
|
|
QCryptoTLSCredsAnon *acreds = QCRYPTO_TLS_CREDS_ANON(creds);
|
|
char *prio;
|
|
|
|
if (creds->priority != NULL) {
|
|
prio = g_strdup_printf("%s:+ANON-DH", creds->priority);
|
|
} else {
|
|
prio = g_strdup(CONFIG_TLS_PRIORITY ":+ANON-DH");
|
|
}
|
|
|
|
ret = gnutls_priority_set_direct(session->handle, prio, NULL);
|
|
if (ret < 0) {
|
|
error_setg(errp, "Unable to set TLS session priority %s: %s",
|
|
prio, gnutls_strerror(ret));
|
|
g_free(prio);
|
|
goto error;
|
|
}
|
|
g_free(prio);
|
|
if (creds->endpoint == QCRYPTO_TLS_CREDS_ENDPOINT_SERVER) {
|
|
ret = gnutls_credentials_set(session->handle,
|
|
GNUTLS_CRD_ANON,
|
|
acreds->data.server);
|
|
} else {
|
|
ret = gnutls_credentials_set(session->handle,
|
|
GNUTLS_CRD_ANON,
|
|
acreds->data.client);
|
|
}
|
|
if (ret < 0) {
|
|
error_setg(errp, "Cannot set session credentials: %s",
|
|
gnutls_strerror(ret));
|
|
goto error;
|
|
}
|
|
} else if (object_dynamic_cast(OBJECT(creds),
|
|
TYPE_QCRYPTO_TLS_CREDS_X509)) {
|
|
QCryptoTLSCredsX509 *tcreds = QCRYPTO_TLS_CREDS_X509(creds);
|
|
const char *prio = creds->priority;
|
|
if (!prio) {
|
|
prio = CONFIG_TLS_PRIORITY;
|
|
}
|
|
|
|
ret = gnutls_priority_set_direct(session->handle, prio, NULL);
|
|
if (ret < 0) {
|
|
error_setg(errp, "Cannot set default TLS session priority %s: %s",
|
|
prio, gnutls_strerror(ret));
|
|
goto error;
|
|
}
|
|
ret = gnutls_credentials_set(session->handle,
|
|
GNUTLS_CRD_CERTIFICATE,
|
|
tcreds->data);
|
|
if (ret < 0) {
|
|
error_setg(errp, "Cannot set session credentials: %s",
|
|
gnutls_strerror(ret));
|
|
goto error;
|
|
}
|
|
|
|
if (creds->endpoint == QCRYPTO_TLS_CREDS_ENDPOINT_SERVER) {
|
|
/* This requests, but does not enforce a client cert.
|
|
* The cert checking code later does enforcement */
|
|
gnutls_certificate_server_set_request(session->handle,
|
|
GNUTLS_CERT_REQUEST);
|
|
}
|
|
} else {
|
|
error_setg(errp, "Unsupported TLS credentials type %s",
|
|
object_get_typename(OBJECT(creds)));
|
|
goto error;
|
|
}
|
|
|
|
gnutls_transport_set_ptr(session->handle, session);
|
|
gnutls_transport_set_push_function(session->handle,
|
|
qcrypto_tls_session_push);
|
|
gnutls_transport_set_pull_function(session->handle,
|
|
qcrypto_tls_session_pull);
|
|
|
|
return session;
|
|
|
|
error:
|
|
qcrypto_tls_session_free(session);
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
qcrypto_tls_session_check_certificate(QCryptoTLSSession *session,
|
|
Error **errp)
|
|
{
|
|
int ret;
|
|
unsigned int status;
|
|
const gnutls_datum_t *certs;
|
|
unsigned int nCerts, i;
|
|
time_t now;
|
|
gnutls_x509_crt_t cert = NULL;
|
|
|
|
now = time(NULL);
|
|
if (now == ((time_t)-1)) {
|
|
error_setg_errno(errp, errno, "Cannot get current time");
|
|
return -1;
|
|
}
|
|
|
|
ret = gnutls_certificate_verify_peers2(session->handle, &status);
|
|
if (ret < 0) {
|
|
error_setg(errp, "Verify failed: %s", gnutls_strerror(ret));
|
|
return -1;
|
|
}
|
|
|
|
if (status != 0) {
|
|
const char *reason = "Invalid certificate";
|
|
|
|
if (status & GNUTLS_CERT_INVALID) {
|
|
reason = "The certificate is not trusted";
|
|
}
|
|
|
|
if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) {
|
|
reason = "The certificate hasn't got a known issuer";
|
|
}
|
|
|
|
if (status & GNUTLS_CERT_REVOKED) {
|
|
reason = "The certificate has been revoked";
|
|
}
|
|
|
|
if (status & GNUTLS_CERT_INSECURE_ALGORITHM) {
|
|
reason = "The certificate uses an insecure algorithm";
|
|
}
|
|
|
|
error_setg(errp, "%s", reason);
|
|
return -1;
|
|
}
|
|
|
|
certs = gnutls_certificate_get_peers(session->handle, &nCerts);
|
|
if (!certs) {
|
|
error_setg(errp, "No certificate peers");
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; i < nCerts; i++) {
|
|
ret = gnutls_x509_crt_init(&cert);
|
|
if (ret < 0) {
|
|
error_setg(errp, "Cannot initialize certificate: %s",
|
|
gnutls_strerror(ret));
|
|
return -1;
|
|
}
|
|
|
|
ret = gnutls_x509_crt_import(cert, &certs[i], GNUTLS_X509_FMT_DER);
|
|
if (ret < 0) {
|
|
error_setg(errp, "Cannot import certificate: %s",
|
|
gnutls_strerror(ret));
|
|
goto error;
|
|
}
|
|
|
|
if (gnutls_x509_crt_get_expiration_time(cert) < now) {
|
|
error_setg(errp, "The certificate has expired");
|
|
goto error;
|
|
}
|
|
|
|
if (gnutls_x509_crt_get_activation_time(cert) > now) {
|
|
error_setg(errp, "The certificate is not yet activated");
|
|
goto error;
|
|
}
|
|
|
|
if (gnutls_x509_crt_get_activation_time(cert) > now) {
|
|
error_setg(errp, "The certificate is not yet activated");
|
|
goto error;
|
|
}
|
|
|
|
if (i == 0) {
|
|
size_t dnameSize = 1024;
|
|
session->peername = g_malloc(dnameSize);
|
|
requery:
|
|
ret = gnutls_x509_crt_get_dn(cert, session->peername, &dnameSize);
|
|
if (ret < 0) {
|
|
if (ret == GNUTLS_E_SHORT_MEMORY_BUFFER) {
|
|
session->peername = g_realloc(session->peername,
|
|
dnameSize);
|
|
goto requery;
|
|
}
|
|
error_setg(errp, "Cannot get client distinguished name: %s",
|
|
gnutls_strerror(ret));
|
|
goto error;
|
|
}
|
|
if (session->aclname) {
|
|
qemu_acl *acl = qemu_acl_find(session->aclname);
|
|
int allow;
|
|
if (!acl) {
|
|
error_setg(errp, "Cannot find ACL %s",
|
|
session->aclname);
|
|
goto error;
|
|
}
|
|
|
|
allow = qemu_acl_party_is_allowed(acl, session->peername);
|
|
|
|
if (!allow) {
|
|
error_setg(errp, "TLS x509 ACL check for %s is denied",
|
|
session->peername);
|
|
goto error;
|
|
}
|
|
}
|
|
if (session->hostname) {
|
|
if (!gnutls_x509_crt_check_hostname(cert, session->hostname)) {
|
|
error_setg(errp,
|
|
"Certificate does not match the hostname %s",
|
|
session->hostname);
|
|
goto error;
|
|
}
|
|
}
|
|
}
|
|
|
|
gnutls_x509_crt_deinit(cert);
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
gnutls_x509_crt_deinit(cert);
|
|
return -1;
|
|
}
|
|
|
|
|
|
int
|
|
qcrypto_tls_session_check_credentials(QCryptoTLSSession *session,
|
|
Error **errp)
|
|
{
|
|
if (object_dynamic_cast(OBJECT(session->creds),
|
|
TYPE_QCRYPTO_TLS_CREDS_ANON)) {
|
|
return 0;
|
|
} else if (object_dynamic_cast(OBJECT(session->creds),
|
|
TYPE_QCRYPTO_TLS_CREDS_X509)) {
|
|
if (session->creds->verifyPeer) {
|
|
return qcrypto_tls_session_check_certificate(session,
|
|
errp);
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
error_setg(errp, "Unexpected credential type %s",
|
|
object_get_typename(OBJECT(session->creds)));
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
qcrypto_tls_session_set_callbacks(QCryptoTLSSession *session,
|
|
QCryptoTLSSessionWriteFunc writeFunc,
|
|
QCryptoTLSSessionReadFunc readFunc,
|
|
void *opaque)
|
|
{
|
|
session->writeFunc = writeFunc;
|
|
session->readFunc = readFunc;
|
|
session->opaque = opaque;
|
|
}
|
|
|
|
|
|
ssize_t
|
|
qcrypto_tls_session_write(QCryptoTLSSession *session,
|
|
const char *buf,
|
|
size_t len)
|
|
{
|
|
ssize_t ret = gnutls_record_send(session->handle, buf, len);
|
|
|
|
if (ret < 0) {
|
|
switch (ret) {
|
|
case GNUTLS_E_AGAIN:
|
|
errno = EAGAIN;
|
|
break;
|
|
case GNUTLS_E_INTERRUPTED:
|
|
errno = EINTR;
|
|
break;
|
|
default:
|
|
errno = EIO;
|
|
break;
|
|
}
|
|
ret = -1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
ssize_t
|
|
qcrypto_tls_session_read(QCryptoTLSSession *session,
|
|
char *buf,
|
|
size_t len)
|
|
{
|
|
ssize_t ret = gnutls_record_recv(session->handle, buf, len);
|
|
|
|
if (ret < 0) {
|
|
switch (ret) {
|
|
case GNUTLS_E_AGAIN:
|
|
errno = EAGAIN;
|
|
break;
|
|
case GNUTLS_E_INTERRUPTED:
|
|
errno = EINTR;
|
|
break;
|
|
default:
|
|
errno = EIO;
|
|
break;
|
|
}
|
|
ret = -1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int
|
|
qcrypto_tls_session_handshake(QCryptoTLSSession *session,
|
|
Error **errp)
|
|
{
|
|
int ret = gnutls_handshake(session->handle);
|
|
if (ret == 0) {
|
|
session->handshakeComplete = true;
|
|
} else {
|
|
if (ret == GNUTLS_E_INTERRUPTED ||
|
|
ret == GNUTLS_E_AGAIN) {
|
|
ret = 1;
|
|
} else {
|
|
error_setg(errp, "TLS handshake failed: %s",
|
|
gnutls_strerror(ret));
|
|
ret = -1;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
QCryptoTLSSessionHandshakeStatus
|
|
qcrypto_tls_session_get_handshake_status(QCryptoTLSSession *session)
|
|
{
|
|
if (session->handshakeComplete) {
|
|
return QCRYPTO_TLS_HANDSHAKE_COMPLETE;
|
|
} else if (gnutls_record_get_direction(session->handle) == 0) {
|
|
return QCRYPTO_TLS_HANDSHAKE_RECVING;
|
|
} else {
|
|
return QCRYPTO_TLS_HANDSHAKE_SENDING;
|
|
}
|
|
}
|
|
|
|
|
|
int
|
|
qcrypto_tls_session_get_key_size(QCryptoTLSSession *session,
|
|
Error **errp)
|
|
{
|
|
gnutls_cipher_algorithm_t cipher;
|
|
int ssf;
|
|
|
|
cipher = gnutls_cipher_get(session->handle);
|
|
ssf = gnutls_cipher_get_key_size(cipher);
|
|
if (!ssf) {
|
|
error_setg(errp, "Cannot get TLS cipher key size");
|
|
return -1;
|
|
}
|
|
return ssf;
|
|
}
|
|
|
|
|
|
char *
|
|
qcrypto_tls_session_get_peer_name(QCryptoTLSSession *session)
|
|
{
|
|
if (session->peername) {
|
|
return g_strdup(session->peername);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
#else /* ! CONFIG_GNUTLS */
|
|
|
|
|
|
QCryptoTLSSession *
|
|
qcrypto_tls_session_new(QCryptoTLSCreds *creds G_GNUC_UNUSED,
|
|
const char *hostname G_GNUC_UNUSED,
|
|
const char *aclname G_GNUC_UNUSED,
|
|
QCryptoTLSCredsEndpoint endpoint G_GNUC_UNUSED,
|
|
Error **errp)
|
|
{
|
|
error_setg(errp, "TLS requires GNUTLS support");
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void
|
|
qcrypto_tls_session_free(QCryptoTLSSession *sess G_GNUC_UNUSED)
|
|
{
|
|
}
|
|
|
|
|
|
int
|
|
qcrypto_tls_session_check_credentials(QCryptoTLSSession *sess G_GNUC_UNUSED,
|
|
Error **errp)
|
|
{
|
|
error_setg(errp, "TLS requires GNUTLS support");
|
|
return -1;
|
|
}
|
|
|
|
|
|
void
|
|
qcrypto_tls_session_set_callbacks(
|
|
QCryptoTLSSession *sess G_GNUC_UNUSED,
|
|
QCryptoTLSSessionWriteFunc writeFunc G_GNUC_UNUSED,
|
|
QCryptoTLSSessionReadFunc readFunc G_GNUC_UNUSED,
|
|
void *opaque G_GNUC_UNUSED)
|
|
{
|
|
}
|
|
|
|
|
|
ssize_t
|
|
qcrypto_tls_session_write(QCryptoTLSSession *sess,
|
|
const char *buf,
|
|
size_t len)
|
|
{
|
|
errno = -EIO;
|
|
return -1;
|
|
}
|
|
|
|
|
|
ssize_t
|
|
qcrypto_tls_session_read(QCryptoTLSSession *sess,
|
|
char *buf,
|
|
size_t len)
|
|
{
|
|
errno = -EIO;
|
|
return -1;
|
|
}
|
|
|
|
|
|
int
|
|
qcrypto_tls_session_handshake(QCryptoTLSSession *sess,
|
|
Error **errp)
|
|
{
|
|
error_setg(errp, "TLS requires GNUTLS support");
|
|
return -1;
|
|
}
|
|
|
|
|
|
QCryptoTLSSessionHandshakeStatus
|
|
qcrypto_tls_session_get_handshake_status(QCryptoTLSSession *sess)
|
|
{
|
|
return QCRYPTO_TLS_HANDSHAKE_COMPLETE;
|
|
}
|
|
|
|
|
|
int
|
|
qcrypto_tls_session_get_key_size(QCryptoTLSSession *sess,
|
|
Error **errp)
|
|
{
|
|
error_setg(errp, "TLS requires GNUTLS support");
|
|
return -1;
|
|
}
|
|
|
|
|
|
char *
|
|
qcrypto_tls_session_get_peer_name(QCryptoTLSSession *sess)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
#endif
|