/* GNU Mailutils -- a suite of utilities for electronic mail
Copyright (C) 2003-2021 Free Software Foundation, 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 3 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
. */
#if HAVE_CONFIG_H
# include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static void
_mu_gtls_logger(int level, const char *text)
{
int len = strlen (text);
if (text[len-1] == '\n')
len--;
mu_diag_output (MU_DIAG_DEBUG, "GnuTLS(%d): %.*s", level, len, text);
}
void
mu_deinit_tls_libs (void)
{
if (mu_tls_enable)
gnutls_global_deinit ();
mu_tls_enable = 0;
}
static void
_onexit_deinit (void *ptr MU_ARG_UNUSED)
{
mu_deinit_tls_libs ();
}
int
mu_init_tls_libs (void)
{
if (!mu_tls_enable)
{
int rc = gnutls_global_init ();
if (rc == GNUTLS_E_SUCCESS)
{
mu_tls_enable = 1;
mu_onexit (_onexit_deinit, NULL);
if (mu_debug_level_p (MU_DEBCAT_TLS, MU_DEBUG_PROT))
{
gnutls_global_set_log_function (_mu_gtls_logger);
gnutls_global_set_log_level (110);
}
}
else
{
mu_error ("gnutls_global_init: %s", gnutls_strerror (rc));
}
}
return mu_tls_enable;
}
/* TLS push & pull functions */
static ssize_t
_tls_stream_pull (gnutls_transport_ptr_t fd, void *buf, size_t size)
{
mu_stream_t stream = fd;
int rc;
size_t rdbytes;
while ((rc = mu_stream_read (stream, buf, size, &rdbytes)) == EAGAIN)
;
if (rc)
return -1;
return rdbytes;
}
static ssize_t
_tls_stream_push (gnutls_transport_ptr_t fd, const void *buf, size_t size)
{
mu_stream_t stream = fd;
int rc;
rc = mu_stream_write (stream, buf, size, &size);
if (rc)
{
mu_error ("_tls_stream_push: %s",
mu_stream_strerror (stream, rc)); /* FIXME */
return -1;
}
mu_stream_flush (stream);
return size;
}
static char default_priority_string[] = "NORMAL";
static int
prep_session (mu_stream_t stream)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
gnutls_certificate_credentials_t cred = NULL;
mu_transport_t transport[2];
int rc;
const char *errp;
if (!sp->cred)
{
rc = gnutls_certificate_allocate_credentials (&cred);
if (rc)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("gnutls_certificate_allocate_credentials: %s",
gnutls_strerror (rc)));
sp->tls_err = rc;
return MU_ERR_FAILURE;
}
if (sp->conf.ca_file)
{
rc = gnutls_certificate_set_x509_trust_file (cred, sp->conf.ca_file,
GNUTLS_X509_FMT_PEM);
if (rc < 0)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("can't use X509 CA file %s: %s",
sp->conf.ca_file,
gnutls_strerror (rc)));
goto cred_err;
}
}
if (sp->conf.cert_file && sp->conf.key_file)
{
rc = gnutls_certificate_set_x509_key_file (cred,
sp->conf.cert_file,
sp->conf.key_file,
GNUTLS_X509_FMT_PEM);
if (rc != GNUTLS_E_SUCCESS)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("can't use X509 cert/key pair (%s,%s): %s",
sp->conf.cert_file,
sp->conf.key_file,
gnutls_strerror (rc)));
goto cred_err;
}
}
sp->cred = cred;
}
rc = gnutls_init (&sp->session, sp->session_type);
if (rc != GNUTLS_E_SUCCESS)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("failed to initialize session: %s", gnutls_strerror (rc)));
goto cred_err;
}
rc = gnutls_priority_set_direct (sp->session,
sp->conf.priorities
? sp->conf.priorities
: default_priority_string,
&errp);
if (rc != GNUTLS_E_SUCCESS)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("error setting priorities near %s: %s",
errp, gnutls_strerror (rc)));
goto cred_err;
}
rc = gnutls_credentials_set (sp->session, GNUTLS_CRD_CERTIFICATE, sp->cred);
if (rc)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("gnutls_credentials_set: %s", gnutls_strerror (rc)));
goto sess_err;
}
if (sp->session_type == GNUTLS_SERVER)
gnutls_certificate_server_set_request (sp->session, GNUTLS_CERT_REQUEST);
rc = mu_stream_ioctl (stream, MU_IOCTL_TRANSPORT, MU_IOCTL_OP_GET,
transport);
if (rc)
{
mu_diag_funcall (MU_DIAG_ERROR, "mu_stream_ioctl", NULL, rc);
abort (); /* should not happen */
}
gnutls_transport_set_ptr2 (sp->session,
(gnutls_transport_ptr_t) transport[0],
(gnutls_transport_ptr_t) transport[1]);
gnutls_transport_set_pull_function (sp->session, _tls_stream_pull);
gnutls_transport_set_push_function (sp->session, _tls_stream_push);
return 0;
sess_err:
gnutls_deinit (sp->session);
cred_err:
if (sp->cred)
{
gnutls_certificate_free_credentials (sp->cred);
sp->cred = NULL;
}
sp->tls_err = rc;
return MU_ERR_FAILURE;
}
static int
_tls_open (mu_stream_t stream)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
int rc = 0;
switch (sp->state)
{
case state_closed:
if (sp->session)
{
gnutls_deinit (sp->session);
sp->session = NULL;
}
/* FALLTHROUGH */
case state_init:
rc = prep_session (stream);
if (rc)
break;
rc = gnutls_handshake (sp->session);
if (rc != GNUTLS_E_SUCCESS)
{
mu_debug (MU_DEBCAT_STREAM, MU_DEBUG_ERROR,
("gnutls_handshake: %s", gnutls_strerror (rc)));
sp->tls_err = rc;
gnutls_deinit (sp->session);
sp->session = NULL;
sp->state = state_init;
}
else
/* FIXME: if (ssl_cafile) verify_certificate (s->session); */
sp->state = state_open;
break;
default:
rc = MU_ERR_BADOP;
}
return rc;
}
/* TLS stream */
static int
_tls_read (struct _mu_stream *str, char *buf, size_t bufsize,
size_t *pnread)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *)str;
return mu_stream_read (sp->transport[0], buf, bufsize, pnread);
}
static int
_tls_write (struct _mu_stream *str, const char *buf, size_t bufsize,
size_t *pnwrite)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *)str;
return mu_stream_write (sp->transport[1], buf, bufsize, pnwrite);
}
static int
get_cipher_info (gnutls_session_t session, mu_property_t *pprop)
{
mu_property_t prop;
const char *s;
int rc;
if (!pprop)
return EINVAL;
rc = mu_property_create_init (&prop, mu_assoc_property_init, NULL);
if (rc)
return rc;
s = gnutls_protocol_get_name (gnutls_protocol_get_version (session));
mu_property_set_value (prop, "protocol", s, 1);
s = gnutls_cipher_get_name (gnutls_cipher_get (session));
mu_property_set_value (prop, "cipher", s, 1);
s = gnutls_mac_get_name (gnutls_mac_get (session));
mu_property_set_value (prop, "mac", s, 1);
*pprop = prop;
return 0;
}
static int
_tls_ioctl (struct _mu_stream *stream, int code, int opcode, void *arg)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
switch (code)
{
case MU_IOCTL_TRANSPORT:
switch (opcode)
{
case MU_IOCTL_OP_GET:
if (!arg)
return EINVAL;
else
{
mu_transport_t *ptrans, trans[2];
ptrans = arg;
mu_stream_ioctl (sp->transport[0], MU_IOCTL_TRANSPORT,
MU_IOCTL_OP_GET, trans);
ptrans[0] = trans[0];
mu_stream_ioctl (sp->transport[1], MU_IOCTL_TRANSPORT,
MU_IOCTL_OP_GET, trans);
ptrans[1] = trans[0];
}
break;
case MU_IOCTL_OP_SET:
return ENOSYS;
default:
return EINVAL;
}
break;
case MU_IOCTL_TRANSPORT_BUFFER:
if (!arg)
return EINVAL;
else
{
struct mu_buffer_query *qp = arg;
switch (opcode)
{
case MU_IOCTL_OP_GET:
if (!MU_TRANSPORT_VALID_TYPE (qp->type) ||
!sp->transport[qp->type])
return EINVAL;
return mu_stream_get_buffer (sp->transport[qp->type], qp);
case MU_IOCTL_OP_SET:
if (!MU_TRANSPORT_VALID_TYPE (qp->type) ||
!sp->transport[qp->type])
return EINVAL;
return mu_stream_set_buffer (sp->transport[qp->type],
qp->buftype, qp->bufsize);
default:
return EINVAL;
}
}
break;
case MU_IOCTL_TLSSTREAM:
switch (opcode)
{
case MU_IOCTL_TLS_GET_CIPHER_INFO:
return get_cipher_info (sp->session, arg);
default:
return EINVAL;
}
break;
case MU_IOCTL_TCPSTREAM:
return mu_stream_ioctl (sp->transport[0], code, opcode, arg);
default:
return ENOSYS;
}
return 0;
}
static int
_tls_wait (struct _mu_stream *stream, int *pflags, struct timeval *tvp)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
int rc = EINVAL;
if (*pflags == MU_STREAM_READY_RD)
rc = mu_stream_wait (sp->transport[0], pflags, tvp);
else if (*pflags == MU_STREAM_READY_WR)
rc = mu_stream_wait (sp->transport[1], pflags, tvp);
return rc;
}
static int
_tls_flush (struct _mu_stream *stream)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
return mu_stream_flush (sp->transport[1]);
}
static int
_tls_close (mu_stream_t stream)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
if (sp->session && sp->state == state_open)
{
gnutls_bye (sp->session, GNUTLS_SHUT_RDWR);
sp->state = state_closed;
}
mu_stream_close (sp->transport[0]);
mu_stream_close (sp->transport[1]);
return 0;
}
static void free_conf (struct mu_tls_config *conf);
static void
_tls_done (struct _mu_stream *stream)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
if (sp->session)
gnutls_deinit (sp->session);
if (sp->cred)
gnutls_certificate_free_credentials (sp->cred);
free_conf (&sp->conf);
mu_stream_destroy (&sp->transport[0]);
mu_stream_destroy (&sp->transport[1]);
}
static const char *
_tls_error_string (struct _mu_stream *stream, int rc)
{
if (rc == EIO)
{
struct _mu_tls_stream *sp = (struct _mu_tls_stream *) stream;
return gnutls_strerror (sp->tls_err);
}
return mu_strerror (rc);
}
static void
free_conf (struct mu_tls_config *conf)
{
free (conf->cert_file);
free (conf->key_file);
free (conf->ca_file);
}
static int
copy_conf (struct mu_tls_config *dst, struct mu_tls_config const *src)
{
if (src->cert_file)
{
dst->cert_file = strdup (src->cert_file);
if (!dst->cert_file)
return errno;
}
else
dst->cert_file = NULL;
if (src->key_file)
{
dst->key_file = strdup (src->key_file);
if (!dst->cert_file)
{
int rc = errno;
free (dst->cert_file);
return rc;
}
}
else
dst->key_file = NULL;
if (src->ca_file)
{
dst->ca_file = strdup (src->ca_file);
if (!dst->ca_file)
{
int rc = errno;
free (dst->cert_file);
free (dst->key_file);
return rc;
}
}
else
dst->ca_file = NULL;
return 0;
}
int
mu_tls_stream_create (mu_stream_t *pstream,
mu_stream_t strin, mu_stream_t strout,
struct mu_tls_config const *conf,
enum mu_tls_type type,
int flags)
{
struct _mu_tls_stream *sp;
int rc;
mu_stream_t stream;
int session_type;
if (!pstream)
return MU_ERR_OUT_PTR_NULL;
if (!conf || !strin || !strout)
return EINVAL;
if (!mu_init_tls_libs ())
return ENOSYS;
switch (mu_tls_config_check (conf, 1))
{
case MU_TLS_CONFIG_OK:
case MU_TLS_CONFIG_NULL:
break;
case MU_TLS_CONFIG_UNSAFE:
return EACCES;
case MU_TLS_CONFIG_FAIL:
return ENOENT;
}
switch (type)
{
case MU_TLS_CLIENT:
session_type = GNUTLS_CLIENT;
break;
case MU_TLS_SERVER:
session_type = GNUTLS_SERVER;
break;
default:
return EINVAL;
}
sp = (struct _mu_tls_stream *)
_mu_stream_create (sizeof (*sp), MU_STREAM_RDWR);
if (!sp)
return ENOMEM;
sp->session_type = session_type;
sp->state = state_init;
sp->session = NULL;
sp->cred = NULL;
rc = copy_conf (&sp->conf, conf);
if (rc)
{
free (sp);
return rc;
}
sp->stream.read = _tls_read;
sp->stream.write = _tls_write;
sp->stream.flush = _tls_flush;
sp->stream.open = _tls_open;
sp->stream.close = _tls_close;
sp->stream.done = _tls_done;
sp->stream.ctl = _tls_ioctl;
sp->stream.wait = _tls_wait;
sp->stream.error_string = _tls_error_string;
mu_stream_set_buffer (strin, mu_buffer_none, 0);
mu_stream_set_buffer (strout, mu_buffer_none, 0);
stream = (mu_stream_t) sp;
rc = mu_tls_io_stream_create (&sp->transport[0], strin,
MU_STREAM_READ, sp);
if (rc)
goto err;
rc = mu_tls_io_stream_create (&sp->transport[1], strout,
MU_STREAM_WRITE, sp);
if (rc)
goto err;
mu_stream_set_buffer (stream, mu_buffer_line, 0);
rc = mu_stream_open (stream);
if (rc)
goto err;
*pstream = stream;
return 0;
err:
mu_stream_destroy (&stream);
return rc;
}
int
mu_tls_client_stream_create (mu_stream_t *pstream,
mu_stream_t strin, mu_stream_t strout, int flags)
{
struct mu_tls_config conf = {
.cert_file = NULL,
.key_file = NULL,
.ca_file = NULL,
.priorities = NULL
};
return mu_tls_stream_create (pstream, strin, strout, &conf, MU_TLS_CLIENT,
flags);
}