/*
 * Copyright (c) 2003-2006 Sendmail, Inc. and its suppliers.
 *	All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 */

/*
**  This code is based on Lutz Jaenicke's TLS patch for postfix.
**  http://www.aet.tu-cottbus.de/personen/jaenicke/pfixtls/
*/

#include "sm/generic.h"
SM_RCSID("@(#)$Id: tlsbio.c,v 1.26 2007/01/20 05:03:31 ca Exp $")
#include "sm/error.h"
#include "sm/assert.h"
#include "sm/io.h"
#include "sm/tls.h"
#include "sm/tlsbio.h"

#if MTA_USE_TLS

static read_F	sm_tlsbio_read;
static write_F	sm_tlsbio_write;
static open_F	sm_tlsbio_open;
static close_F	sm_tlsbio_close;


/* only for communication between tls_open() and sm_tlsbio_open() */
struct tls_info_S
{
	SSL		*ti_con;	/* SSL connection */
	sm_file_T	*ti_fp;		/* underlying file */
	tlsl_ctx_P	 ti_log_ctx;
};
typedef struct tls_info_S tls_info_T, *tls_info_P;

/*
**  Define a maxlength for certificate onelines. The length is checked by
**  all routines when copying.
*/

#define CCERT_BUFSIZ 256

struct tlsbio_ctx_S
{
	SSL		*tbc_con;	/* SSL connection */
	BIO		*tbc_internal_bio; /* application/TLS side of pair */
	BIO		*tbc_network_bio;  /* network side of pair */
	sm_file_T	*tbc_fp;	/* underlying file */
	tlsl_ctx_P	 tbc_log_ctx;
#if 0
	/* this belongs in the session context */
	char		tbc_peer_subject[CCERT_BUFSIZ];
	char		tbc_peer_issuer[CCERT_BUFSIZ];
	char		tbc_peer_CN[CCERT_BUFSIZ];
	char		tbc_issuer_CN[CCERT_BUFSIZ];
	uchar		tbc_md[EVP_MAX_MD_SIZE];
	char		tbc_fingerprint[EVP_MAX_MD_SIZE * 3];
	char		tbc_peername_save[129];
	int		tbc_enforce_verify_errors;
	int		tbc_enforce_CN;
#endif /* 0 */
};
typedef struct tlsbio_ctx_S tlsbio_ctx_T, *tlsbio_ctx_P;

/*
 * DESCRIPTION: Keeping control of the network interface using BIO-pairs.
 *
 * When the TLS layer is active, all input/output must be filtered through
 * it. On the other hand to handle timeout conditions, full control over
 * the network socket must be kept. This rules out the "normal way" of
 * connecting the TLS layer directly to the socket.
 * The TLS layer is realized with a BIO-pair:
 *
 * application  |   TLS-engine
 *       |      |
 *       +--------> SSL_operations()
 *              |     /\    ||
 *              |     ||    \/
 *              |   BIO-pair (internal_bio)
 *       +--------< BIO-pair (network_bio)
 *       |      |
 *     socket   |
 *
 * The normal application operations connect to the SSL operations to send
 * and retrieve (cleartext) data. Inside the TLS-engine the data are converted
 * to/from TLS protocol. The TLS functionality itself is only connected to
 * the internal_bio and hence only has status information about this internal
 * interface.
 * Thus, if the SSL_operations() return successfully (SSL_ERROR_NONE) or want
 * to read (SSL_ERROR_WANT_READ) there may as well be data inside the buffering
 * BIO-pair. So whenever an SSL_operation() returns without a fatal error,
 * the BIO-pair internal buffer must be flushed to the network.
 * NOTE: This is especially true in the SSL_ERROR_WANT_READ case: the TLS-layer
 * might want to read handshake data, that will never come since its own
 * written data will only reach the peer after flushing the buffer!
 *
 * The BIO-pair buffer size has been set to 8192 bytes, this is an arbitrary
 * value that can hold more data than the typical PMTU, so that it does
 * not force the generation of packets smaller than necessary.
 * It is also larger than the default VSTREAM_BUFSIZE (4096, see vstream.h),
 * so that large write operations could be handled within one call.
 * The internal buffer in the network/network_bio handling layer has been
 * set to the same value, since this seems to be reasonable. The code is
 * however able to handle arbitrary values smaller or larger than the
 * buffer size in the BIO-pair.
 *
 **  Note: can we get the PMTU size and set this accordingly?
 */

const ssize_t BIO_bufsiz = 8192;
#define NETLAYER_BUFFERSIZE 8192
#if NETLAYER_BUFFERSIZE >= INT_MAX
ERROR _NETLAYER_BUFFERSIZE >=  _INT_MAX
#endif

/*
 * The interface layer between network and BIO-pair. The BIO-pair buffers
 * the data to/from the TLS layer. Hence, at any time, there may be data
 * in the buffer that must be written to the network. This writing has
 * highest priority because the handshake might fail otherwise.
 * Only then a read_request can be satisfied.
 */

static int
network_biopair_interop(sm_file_T *fp, BIO *network_bio)
{
	int want_write;
	ssize_t num_write;
	int write_pos;
	int from_bio;
	int want_read;
	ssize_t num_read;
	int to_bio;
	sm_ret_T ret;
	char buffer[NETLAYER_BUFFERSIZE];

	while ((want_write = BIO_ctrl_pending(network_bio)) > 0)
	{
		if (want_write > (int)sizeof(buffer))
			want_write = (int)sizeof(buffer);
		from_bio = BIO_read(network_bio, buffer, want_write);

		/*
		 * Write the complete contents of the buffer. Since TLS performs
		 * underlying handshaking, we cannot afford to leave the buffer
		 * unflushed, as we could run into a deadlock trap (the peer
		 * waiting for a final byte and we already waiting for his reply
		 * in read position).
		 */

		write_pos = 0;
		do
		{
			ret = sm_io_write(fp, buffer + write_pos,
					from_bio - write_pos, &num_write);
			if (sm_is_err(ret))
				return ret;
			if (num_write <= 0)
				return sm_error_perm(SM_EM_TLS, EIO);
			write_pos += num_write;
		} while (write_pos < from_bio);
	}

	while ((want_read = BIO_ctrl_get_read_request(network_bio)) > 0)
	{
		if (want_read > (int)sizeof(buffer))
			want_read = (int)sizeof(buffer);
		ret = sm_io_read(fp, buffer, want_read, &num_read);
		if (sm_is_err(ret))
			return ret;
		if (num_read <= 0)
			return sm_error_perm(SM_EM_TLS, EIO);
		to_bio = BIO_write(network_bio, buffer, num_read);
		if (to_bio != num_read)
			return sm_error_perm(SM_EM_TLS, EIO);
	}
	return 0;
}

/*
**  change format to match standard?
*/

static void
tls_print_errors(tlsl_ctx_P tlsl_ctx)
{
	ulong l, es;
	int line, flags;
	const char *file, *data;
	char buf[256];

	es = CRYPTO_thread_id();
	while ((l = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0)
	{
		if (flags & ERR_TXT_STRING)
			sm_log_write(tlsl_ctx->tlsl_lctx,
				TL_LCAT_IO, TL_LMOD_IO,
				SM_LOG_WARN, 10,
				"%lu:%s:%s:%d:%s", es,
				ERR_error_string(l, buf), file, line, data);
		else
			sm_log_write(tlsl_ctx->tlsl_lctx,
				TL_LCAT_IO, TL_LMOD_IO,
				SM_LOG_WARN, 10,
				"%lu:%s:%s:%d", es,
				ERR_error_string(l, buf), file, line);
	}
}

/*
 * Function to perform the handshake for SSL_accept(), SSL_connect(),
 * and SSL_shutdown() and perform the SSL_read(), SSL_write() operations.
 * Call the underlying network_biopair_interop-layer to make sure the
 * write buffer is flushed after every operation (that did not fail with
 * a fatal error).
 */

/*
XXX Problems:
  How to determine "bytes" (read/written)?
	Is it always the total number or an error?
  uchar *buf doesn't match const uchar *buf for write... use a macro
	or duplicate the code (see sendmail 8 version)?

  turn this into a macro?
../../mta/libmta/tlsbio.c:353: warning: passing arg 5 of `do_tls_operation'
discards qualifiers from pointer target type
*/

int
do_tls_operation(sm_file_T *fp, hsfunc_F *hsfunc, rfunc_F *rfunc, wfunc_F *wfunc, uchar *buf, int num, ssize_t *bytes)
{
	int status;
	int err;
	sm_ret_T ret;
	int biop_retval;
	bool done;
	tlsbio_ctx_T *tlsbio_ctx;

	SM_REQUIRE(bytes != NULL);
	*bytes = 0;
	done = false;
	ret = SM_SUCCESS;
	tlsbio_ctx = (tlsbio_ctx_T *) f_cookie(*fp);
	while (!done)
	{
		if (hsfunc)
			status = hsfunc(tlsbio_ctx->tbc_con);
		else if (rfunc)
			status = rfunc(tlsbio_ctx->tbc_con, buf, num);
		else
			status = wfunc(tlsbio_ctx->tbc_con, (const void *) buf, num);
		err = SSL_get_error(tlsbio_ctx->tbc_con, status);
		switch (err)
		{
		  case SSL_ERROR_NONE:	/* success */
			*bytes = status;
			done = true;	/* no break, flush buffer before leaving */
			/* FALLTHROUGH */

		  case SSL_ERROR_WANT_WRITE:
		  case SSL_ERROR_WANT_READ:
			biop_retval = network_biopair_interop(tlsbio_ctx->tbc_fp,
					     tlsbio_ctx->tbc_network_bio);
			if (biop_retval < 0) {
				if (sm_is_err(biop_retval))
					return biop_retval;
				return (-1);	/* fatal network error */
			}
			break;

		  /* connection was closed cleanly */
		  case SSL_ERROR_ZERO_RETURN:
			/* FALLTHROUGH */

		  /* some error */
		  case SSL_ERROR_SYSCALL:
			ret = sm_error_perm(SM_EM_TLS, SM_SSL_ERROR_SYSCALL);
		  case SSL_ERROR_SSL:
			if (SM_SUCCESS == ret)
				ret = sm_error_perm(SM_EM_TLS, SM_SSL_ERROR_SSL);
		  default:
			if (SM_SUCCESS == ret)
				ret = sm_error_perm(SM_EM_TLS, SM_SSL_ERROR_GENERIC);
			tls_print_errors(tlsbio_ctx->tbc_log_ctx);
			done = true;
		}
	}
	return ret;
}

static sm_ret_T
sm_tlsbio_read(sm_file_T *fp, uchar *buf, size_t n, ssize_t *bytesread)
{
	return do_tls_operation(fp, NULL, SSL_read, NULL, buf, n, bytesread);
}

static sm_ret_T
sm_tlsbio_write(sm_file_T *fp, const uchar *buf, size_t n, ssize_t *byteswritten)
{
	return do_tls_operation(fp, NULL, NULL, SSL_write, (char *) buf, n,
			byteswritten);
}

/*
**  SM_TLSBIO_GETINFO -- returns requested information about a "tls" file
**		 descriptor.
**
**	Parameters:
**		fp -- the file descriptor
**		what -- the type of information requested
**		valp -- the thang to return the information in (unused)
**
**	Returns:
**		-1 for unknown requests
**		>=0 on success with valp filled in (if possible).
*/

/* ARGSUSED2 */
static int
sm_tlsbio_getinfo(sm_file_T *fp, int what, void *valp)
{
	tlsbio_ctx_P tlsbio_ctx;

	SM_REQUIRE(fp != NULL);
	tlsbio_ctx = (tlsbio_ctx_P) f_cookie(*fp);
	switch (what)
	{
	  case SM_IO_WHAT_FD:
		if (tlsbio_ctx->tbc_fp == NULL)
			return -1;
		return f_fd(*(tlsbio_ctx->tbc_fp));
		/* for stdio fileno() compatability */

	  case SM_IO_IS_READABLE:
		return SSL_pending(tlsbio_ctx->tbc_con) > 0;

	  default:
		return -1;
	}
}

/*
**  SM_TLSBIO_OPEN -- creates the tls specific information for opening a
**	       file of the tls type.
**
**	Parameters:
**		fp -- the file pointer associated with the new open
**		info -- the sm_io file pointer holding the open and the
**			TLS encryption connection to be read from or written to
**		flags -- ignored
**		newfp -- new fp
**
**	Returns:
**		0 on success
*/

static sm_ret_T
sm_tlsbio_open(sm_file_T *fp, const void *info, int flags, va_list ap)
{
	tlsbio_ctx_P tlsbio_ctx;
	tls_info_P ti;

	SM_REQUIRE(fp != NULL);
	SM_REQUIRE(info != NULL);
	ti = (tls_info_P) info;
	tlsbio_ctx = (tlsbio_ctx_P) sm_zalloc(sizeof(*tlsbio_ctx));
	if (tlsbio_ctx == NULL)
		return sm_error_temp(SM_EM_TLS, ENOMEM);
	tlsbio_ctx->tbc_fp = ti->ti_fp;
	tlsbio_ctx->tbc_con = ti->ti_con;
	tlsbio_ctx->tbc_log_ctx = ti->ti_log_ctx;

	/* The TLS connection is realized by a BIO_pair, so obtain the pair */
	if (!BIO_new_bio_pair(&tlsbio_ctx->tbc_internal_bio, BIO_bufsiz,
			&tlsbio_ctx->tbc_network_bio, BIO_bufsiz))
		goto error;

	/*
	**  Connect the SSL-connection with the application side
	**  of the BIO-pair for reading and writing.
	*/

	SSL_set_bio(tlsbio_ctx->tbc_con, tlsbio_ctx->tbc_internal_bio,
		 tlsbio_ctx->tbc_internal_bio);

	fp->f_cookie = tlsbio_ctx;
	return SM_SUCCESS;

  error:
	SM_FREE(tlsbio_ctx);
	return sm_error_temp(SM_EM_TLS, SM_E_UNEXPECTED);
	/* XXX Better error! (check OpenSSL error stack?) */
}

/*
**  SM_TLSBIO_CLOSE -- close the tls specific parts of the tls file pointer
**
**	Parameters:
**		fp -- the file pointer to close
**		flags -- ignored
**
**	Returns:
**		0 on success
*/

static sm_ret_T
sm_tlsbio_close(sm_file_T *fp, int flags)
{
	int r;
	ssize_t d;
	tlsbio_ctx_P tlsbio_ctx;

	if (fp == NULL)
		return SM_SUCCESS;	/* really? */
	tlsbio_ctx = (tlsbio_ctx_P) f_cookie(*fp);
	if (tlsbio_ctx == NULL)
		return SM_SUCCESS;
	if (tlsbio_ctx->tbc_con != NULL) {
		/*
		**  Perform SSL_shutdown() twice, as the first attempt may
		**  return too early: it will only send out the shutdown alert
		**  but it will not wait for the peer's shutdown alert.
		**  Therefore, when we are the first party to send the alert,
		**  we must call SSL_shutdown() again.
		**  On failure we don't want to resume the session,
		**  so we will not perform SSL_shutdown() and the session
		**  will be removed as being bad.
		*/

		r = do_tls_operation(fp, SSL_shutdown, NULL, NULL, NULL, 0, &d);
		if (r == 0)
			r = do_tls_operation(fp, SSL_shutdown, NULL, NULL, NULL, 0, &d);
		/*
		**  Free the SSL structure and the BIOs.
		**  Warning: the internal_bio is connected to the SSL structure
		**  and is automatically freed with it. Do not free it again
		**  (core dump)! Only free the network_bio.
		*/

		SSL_free(tlsbio_ctx->tbc_con);
		BIO_free(tlsbio_ctx->tbc_network_bio);
	}
	if (tlsbio_ctx->tbc_fp != NULL) {
		sm_io_close(tlsbio_ctx->tbc_fp, flags);
		tlsbio_ctx->tbc_fp = NULL;
	}
	sm_free_size(tlsbio_ctx, sizeof(*tlsbio_ctx));
	tlsbio_ctx = NULL;
	return SM_SUCCESS;
}

/*
**  TLS_OPEN -- create tls file type and open in and out file pointers
**	      for sendmail to read from and write to.
**
**	Parameters:
**		fp -- lower level file
**		con -- the SSL connection pointer
**		tlsl_ctx -- TLS library context
**		newfp -- (pointer to) new fp (output)
**
**	Returns:
*/

sm_ret_T
tls_open(sm_file_T *fp, SSL *con, tlsl_ctx_P tlsl_ctx, sm_file_T **newfp)
{
	tls_info_T info;
	sm_stream_T tls_vector =
		SM_STREAM_STRUCT(sm_tlsbio_open, sm_tlsbio_close,
			sm_tlsbio_read, sm_tlsbio_write, NULL, NULL, NULL,
			sm_tlsbio_getinfo, NULL);

	SM_REQUIRE(con != NULL);
	SM_REQUIRE(fp != NULL);
	SM_REQUIRE(newfp != NULL);

	info.ti_fp = fp;
	info.ti_con = con;
	info.ti_log_ctx = tlsl_ctx;
	return sm_io_open(&tls_vector, &info, SM_IO_RDWR, newfp);
}
#endif /* MTA_USE_TLS */


syntax highlighted by Code2HTML, v. 0.9.1