-
-/*
- * netsec.c -- Network security routines for handling protocols that
+/* netsec.c -- Network security routines for handling protocols that
* require SASL and/or TLS.
*
* This code is Copyright (c) 2016, by the authors of nmh. See the
#include <h/mh.h>
#include <h/utils.h>
#include <h/netsec.h>
+#include <h/oauth.h>
+#include <stdarg.h>
+#include <sys/select.h>
#ifdef CYRUS_SASL
#include <sasl/sasl.h>
Provide this prototype for earlier versions. */
typedef int (*sasl_callback_ft)();
# endif /* SASL_VERSION_FULL < 0x020125 */
+
+static int netsec_get_user(void *context, int id, const char **result,
+ unsigned int *len);
+static int netsec_get_password(sasl_conn_t *conn, void *context, int id,
+ sasl_secret_t **psecret);
+
+static int sasl_initialized = 0;
+
+#define SASL_MAXRECVBUF 65536
#endif /* CYRUS_SASL */
#ifdef TLS_SUPPORT
#include <openssl/err.h>
static int tls_initialized = 0;
+static SSL_CTX *sslctx = NULL; /* SSL Context */
#endif /* TLS_SUPPORT */
+/* I'm going to hardcode this for now; maybe make it adjustable later? */
+#define NETSEC_BUFSIZE 65536
+
/*
* Our context structure, which holds all of the relevant information
* about a connection.
*/
struct _netsec_context {
- int ns_fd; /* Descriptor for network connection */
+ int ns_readfd; /* Read descriptor for network connection */
+ int ns_writefd; /* Write descriptor for network connection */
+ int ns_noclose; /* Do not close file descriptors if set */
int ns_snoop; /* If true, display network data */
+ int ns_snoop_noend; /* If true, didn't get a CR/LF on last line */
+ netsec_snoop_callback *ns_snoop_cb; /* Snoop output callback */
+ void *ns_snoop_context; /* Context data for snoop function */
+ int ns_timeout; /* Network read timeout, in seconds */
+ char *ns_userid; /* Userid for authentication */
+ char *ns_hostname; /* Hostname we've connected to */
unsigned char *ns_inbuffer; /* Our read input buffer */
unsigned char *ns_inptr; /* Our read buffer input pointer */
unsigned int ns_inbuflen; /* Length of data in input buffer */
unsigned char *ns_outptr; /* Output buffer pointer */
unsigned int ns_outbuflen; /* Output buffer data length */
unsigned int ns_outbufsize; /* Output buffer size */
+ char *sasl_mech; /* User-requested mechanism */
+ char *sasl_chosen_mech; /* Mechanism chosen by SASL */
+ netsec_sasl_callback sasl_proto_cb; /* SASL callback we use */
+#ifdef OAUTH_SUPPORT
+ char *oauth_service; /* OAuth2 service name */
+#endif /* OAUTH_SUPPORT */
#ifdef CYRUS_SASL
sasl_conn_t *sasl_conn; /* SASL connection context */
sasl_ssf_t sasl_ssf; /* SASL Security Strength Factor */
+ sasl_callback_t *sasl_cbs; /* Callbacks used by SASL */
+ nmh_creds_t sasl_creds; /* Credentials (username/password) */
+ sasl_secret_t *sasl_secret; /* SASL password structure */
+ int sasl_seclayer; /* If true, SASL security layer is enabled */
+ char *sasl_tmpbuf; /* Temporary read buffer for decodes */
+ size_t sasl_maxbufsize; /* Maximum negotiated SASL buffer size */
#endif /* CYRUS_SASL */
#ifdef TLS_SUPPORT
- SSL_CTX *sslctx; /* SSL Context */
- SSL *ssl; /* SSL connection information */
BIO *ssl_io; /* BIO used for connection I/O */
+ int tls_active; /* If true, TLS is running */
#endif /* TLS_SUPPORT */
};
+/*
+ * Function to read data from the actual network socket
+ */
+
+static int netsec_fillread(netsec_context *ns_context, char **errstr);
+
+/*
+ * Code to check the ASCII content of a byte array.
+ */
+
+static int checkascii(const unsigned char *byte, size_t len);
+
/*
* How this code works, in general.
*
- * _If_ we are using no encryption or SASL encryption, then we buffer the
- * network data through ns_inbuffer and ns_outbuffer. That should be
- * relatively self-explanatory.
+ * _If_ we are using no encryption then we buffer the network data
+ * through ns_inbuffer and ns_outbuffer. That should be relatively
+ * self-explanatory.
*
- * If we are using SSL for encryption, then use a buffering BIO for output
- * (that just easier). Still do buffering for reads; when we need more
- * data we call the BIO_read() function to fill our local buffer.
+ * If we use encryption, then ns_inbuffer and ns_outbuffer contain the
+ * cleartext data. When it comes time to send the encrypted data on the
+ * (either from a flush or the buffer is full) we either use BIO_write()
+ * for TLS or sasl_encode() (followed by a write() for Cyrus-SASL. For
+ * reads we either use BIO_read() (TLS) or do a network read into a
+ * temporary buffer and use sasl_decode() (Cyrus-SASL). Note that if
+ * negotiate TLS then we disable SASL encryption.
+ *
+ * We used to use a buffering BIO for the reads/writes for TLS, but it
+ * ended up being complicated to special-case the buffering for everything
+ * except TLS, so the buffering is now unified, no matter which encryption
+ * method is being used (even none).
+ *
+ * For SASL authentication, we make use of (for now) the Cyrus-SASL
+ * library. For some mechanisms, we implement those mechanisms directly
+ * since the Cyrus SASL library doesn't support them (like OAuth).
+ */
+
+/*
+ * Allocate and initialize our security context
*/
netsec_context *
netsec_init(void)
{
- netsec_context *nsc = mh_xmalloc(sizeof(*nsc));
+ netsec_context *nsc;
- nsc->ns_fd = -1;
+ NEW(nsc);
+ nsc->ns_readfd = -1;
+ nsc->ns_writefd = -1;
+ nsc->ns_noclose = 0;
nsc->ns_snoop = 0;
-
+ nsc->ns_snoop_noend = 0;
+ nsc->ns_snoop_cb = NULL;
+ nsc->ns_snoop_context = NULL;
+ nsc->ns_userid = NULL;
+ nsc->ns_hostname = NULL;
+ nsc->ns_timeout = 60; /* Our default */
+ nsc->ns_inbufsize = NETSEC_BUFSIZE;
+ nsc->ns_inbuffer = mh_xmalloc(nsc->ns_inbufsize);
+ nsc->ns_inptr = nsc->ns_inbuffer;
+ nsc->ns_inbuflen = 0;
+ nsc->ns_outbufsize = NETSEC_BUFSIZE;
+ nsc->ns_outbuffer = mh_xmalloc(nsc->ns_outbufsize);
+ nsc->ns_outptr = nsc->ns_outbuffer;
+ nsc->ns_outbuflen = 0;
+ nsc->sasl_mech = NULL;
+ nsc->sasl_chosen_mech = NULL;
+ nsc->sasl_proto_cb = NULL;
+#ifdef OAUTH_SUPPORT
+ nsc->oauth_service = NULL;
+#endif /* OAUTH_SUPPORT */
+#ifdef CYRUS_SASL
+ nsc->sasl_conn = NULL;
+ nsc->sasl_cbs = NULL;
+ nsc->sasl_creds = NULL;
+ nsc->sasl_secret = NULL;
+ nsc->sasl_ssf = 0;
+ nsc->sasl_seclayer = 0;
+ nsc->sasl_tmpbuf = NULL;
+ nsc->sasl_maxbufsize = 0;
+#endif /* CYRUS_SASL */
+#ifdef TLS_SUPPORT
+ nsc->ssl_io = NULL;
+ nsc->tls_active = 0;
+#endif /* TLS_SUPPORT */
return nsc;
}
+
+/*
+ * Shutdown the connection completely and free all resources.
+ */
+
+void
+netsec_shutdown(netsec_context *nsc)
+{
+ mh_xfree(nsc->ns_userid);
+ mh_xfree(nsc->ns_hostname);
+ mh_xfree(nsc->ns_inbuffer);
+ mh_xfree(nsc->ns_outbuffer);
+ mh_xfree(nsc->sasl_mech);
+ mh_xfree(nsc->sasl_chosen_mech);
+#ifdef OAUTH_SERVICE
+ mh_xfree(nsc->oauth_service);
+#endif /* OAUTH_SERVICE */
+#ifdef CYRUS_SASL
+ if (nsc->sasl_conn)
+ sasl_dispose(&nsc->sasl_conn);
+ mh_xfree(nsc->sasl_cbs);
+ if (nsc->sasl_creds)
+ nmh_credentials_free(nsc->sasl_creds);
+ if (nsc->sasl_secret) {
+ if (nsc->sasl_secret->len > 0) {
+ memset(nsc->sasl_secret->data, 0, nsc->sasl_secret->len);
+ }
+ free(nsc->sasl_secret);
+ }
+ mh_xfree(nsc->sasl_tmpbuf);
+#endif /* CYRUS_SASL */
+#ifdef TLS_SUPPORT
+ if (nsc->ssl_io)
+ /*
+ * I checked; BIO_free_all() will cause SSL_shutdown to be called
+ * on the SSL object in the chain.
+ */
+ BIO_free_all(nsc->ssl_io);
+#endif /* TLS_SUPPORT */
+
+ if (! nsc->ns_noclose) {
+ if (nsc->ns_readfd != -1)
+ close(nsc->ns_readfd);
+ if (nsc->ns_writefd != -1 && nsc->ns_writefd != nsc->ns_readfd)
+ close(nsc->ns_writefd);
+ }
+
+ free(nsc);
+}
+
+/*
+ * Set the file descriptor for our context
+ */
+
+void
+netsec_set_fd(netsec_context *nsc, int readfd, int writefd)
+{
+ nsc->ns_readfd = readfd;
+ nsc->ns_writefd = writefd;
+}
+
+/*
+ * Set the userid used for authentication for this context
+ */
+
+void
+netsec_set_userid(netsec_context *nsc, const char *userid)
+{
+ nsc->ns_userid = getcpy(userid);
+}
+
+/*
+ * Set the hostname of the remote host we're connecting to.
+ */
+
+void
+netsec_set_hostname(netsec_context *nsc, const char *hostname)
+{
+ nsc->ns_hostname = mh_xstrdup(hostname);
+}
+
+/*
+ * Get the snoop flag for this connection
+ */
+
+int
+netsec_get_snoop(netsec_context *nsc)
+{
+ return nsc->ns_snoop;
+}
+
+/*
+ * Set the snoop flag for this connection
+ */
+
+void
+netsec_set_snoop(netsec_context *nsc, int snoop)
+{
+ nsc->ns_snoop = snoop;
+}
+
+/*
+ * Set the snoop callback for this connection.
+ */
+
+void netsec_set_snoop_callback(netsec_context *nsc,
+ netsec_snoop_callback callback, void *context)
+{
+ nsc->ns_snoop_cb = callback;
+ nsc->ns_snoop_context = context;
+}
+
+/*
+ * A base64-decoding snoop callback
+ */
+
+void
+netsec_b64_snoop_decoder(netsec_context *nsc, const char *string, size_t len,
+ void *context)
+{
+ unsigned char *decoded;
+ size_t decodedlen;
+ int offset;
+ NMH_UNUSED(nsc);
+
+ offset = context ? *((int *) context) : 0;
+
+ if (offset > 0) {
+ /*
+ * Output non-base64 data first.
+ */
+ fprintf(stderr, "%.*s", offset, string);
+ string += offset;
+ len -= offset;
+ }
+
+ if (decodeBase64(string, &decoded, &decodedlen, 1, NULL) == OK) {
+ /*
+ * Some mechanisms produce large binary tokens, which aren't really
+ * readable. So let's do a simple heuristic. If the token is greater
+ * than 100 characters _and_ the first 100 bytes are more than 50%
+ * non-ASCII, then don't print the decoded buffer, just the
+ * base64 text.
+ */
+ if (decodedlen > 100 && !checkascii(decoded, 100)) {
+ fprintf(stderr, "%.*s\n", (int) len, string);
+ } else {
+ char *hexified;
+ hexify(decoded, decodedlen, &hexified);
+ fprintf(stderr, "b64<%s>\n", hexified);
+ free(hexified);
+ }
+ free(decoded);
+ } else {
+ fprintf(stderr, "%.*s\n", (int) len, string);
+ }
+}
+
+/*
+ * If the ASCII content is > 50%, return 1
+ */
+
+static int
+checkascii(const unsigned char *bytes, size_t len)
+{
+ size_t count = 0, half = len / 2;
+
+ while (len-- > 0) {
+ if (isascii(*bytes) && isprint(*bytes) && ++count > half)
+ return 1;
+ bytes++;
+ /* No chance by this point */
+ if (count + len < half)
+ return 0;
+ }
+
+ return 0;
+}
+
+/*
+ * Set the read timeout for this connection
+ */
+
+void
+netsec_set_timeout(netsec_context *nsc, int timeout)
+{
+ nsc->ns_timeout = timeout;
+}
+
+/*
+ * Read data from the network. Basically, return anything in our buffer,
+ * otherwise fill from the network.
+ */
+
+ssize_t
+netsec_read(netsec_context *nsc, void *buffer, size_t size, char **errstr)
+{
+ int retlen;
+
+ /*
+ * If our buffer is empty, then we should fill it now
+ */
+
+ if (nsc->ns_inbuflen == 0) {
+ if (netsec_fillread(nsc, errstr) != OK)
+ return NOTOK;
+ }
+
+ /*
+ * netsec_fillread only returns if the buffer is full, so we can
+ * assume here that this has something in it.
+ */
+
+ retlen = min(size, nsc->ns_inbuflen);
+
+ memcpy(buffer, nsc->ns_inptr, retlen);
+
+ if (retlen == (int) nsc->ns_inbuflen) {
+ /*
+ * We've emptied our buffer, so reset everything.
+ */
+ nsc->ns_inptr = nsc->ns_inbuffer;
+ nsc->ns_inbuflen = 0;
+ } else {
+ nsc->ns_inptr += size;
+ nsc->ns_inbuflen -= size;
+ }
+
+ return OK;
+}
+
+/*
+ * Get a "line" (CR/LF) terminated from the network.
+ *
+ * Okay, we play some games here, so pay attention:
+ *
+ * - Unlike every other function, we return a pointer to the
+ * existing buffer. This pointer is valid until you call another
+ * read function again.
+ * - We NUL-terminate the buffer right at the end, before the CR-LF terminator.
+ * - Technically we look for a LF; if we find a CR right before it, then
+ * we back up one.
+ * - If your data may contain embedded NULs, this won't work. You should
+ * be using netsec_read() in that case.
+ */
+
+char *
+netsec_readline(netsec_context *nsc, size_t *len, char **errstr)
+{
+ unsigned char *ptr = nsc->ns_inptr;
+ size_t count = 0, offset;
+
+retry:
+ /*
+ * Search through our existing buffer for a LF
+ */
+
+ while (count < nsc->ns_inbuflen) {
+ count++;
+ if (*ptr++ == '\n') {
+ char *sptr = (char *) nsc->ns_inptr;
+ if (count > 1 && *(ptr - 2) == '\r')
+ ptr--;
+ *--ptr = '\0';
+ if (len)
+ *len = ptr - nsc->ns_inptr;
+ nsc->ns_inptr += count;
+ nsc->ns_inbuflen -= count;
+ if (nsc->ns_snoop) {
+#ifdef CYRUS_SASL
+ if (nsc->sasl_seclayer)
+ fprintf(stderr, "(sasl-decrypted) ");
+#endif /* CYRUS_SASL */
+#ifdef TLS_SUPPORT
+ if (nsc->tls_active)
+ fprintf(stderr, "(tls-decrypted) ");
+#endif /* TLS_SUPPORT */
+ fprintf(stderr, "<= ");
+ if (nsc->ns_snoop_cb)
+ nsc->ns_snoop_cb(nsc, sptr, strlen(sptr),
+ nsc->ns_snoop_context);
+ else
+ fprintf(stderr, "%s\n", sptr);
+ }
+ return sptr;
+ }
+ }
+
+ /*
+ * Hm, we didn't find a \n. If we've already searched half of the input
+ * buffer, return an error.
+ */
+
+ if (count >= nsc->ns_inbufsize / 2) {
+ netsec_err(errstr, "Unable to find a line terminator after %d bytes",
+ count);
+ return NULL;
+ }
+
+ /*
+ * Okay, get some more network data. This may move inptr, so regenerate
+ * our ptr value;
+ */
+
+ offset = ptr - nsc->ns_inptr;
+
+ if (netsec_fillread(nsc, errstr) != OK)
+ return NULL;
+
+ ptr = nsc->ns_inptr + offset;
+
+ goto retry;
+
+ return NULL; /* Should never reach this */
+}
+
+/*
+ * Fill our read buffer with some data from the network.
+ */
+
+static int
+netsec_fillread(netsec_context *nsc, char **errstr)
+{
+ unsigned char *end;
+ char *readbuf;
+ size_t readbufsize, remaining, startoffset;
+ int rc;
+
+ /*
+ * If inbuflen is zero, that means the buffer has been emptied
+ * completely. In that case move inptr back to the start.
+ */
+
+ if (nsc->ns_inbuflen == 0) {
+ nsc->ns_inptr = nsc->ns_inbuffer;
+ }
+
+#if defined(CYRUS_SASL) || defined(TLS_SUPPORT)
+retry:
+#endif /* CYRUS_SASL || TLS_SUPPORT */
+ /*
+ * If we are using TLS and there's anything pending, then skip the
+ * select call
+ */
+#ifdef TLS_SUPPORT
+ if (!nsc->tls_active || BIO_pending(nsc->ssl_io) == 0)
+#endif /* TLS_SUPPORT */
+ {
+ struct timeval tv;
+ fd_set rfds;
+
+ FD_ZERO(&rfds);
+ FD_SET(nsc->ns_readfd, &rfds);
+
+ tv.tv_sec = nsc->ns_timeout;
+ tv.tv_usec = 0;
+
+ rc = select(nsc->ns_readfd + 1, &rfds, NULL, NULL, &tv);
+
+ if (rc == -1) {
+ netsec_err(errstr, "select() while reading failed: %s",
+ strerror(errno));
+ return NOTOK;
+ }
+
+ if (rc == 0) {
+ netsec_err(errstr, "read() timed out after %d seconds",
+ nsc->ns_timeout);
+ return NOTOK;
+ }
+
+ /*
+ * At this point, we know that rc is 1, so there's not even any
+ * point to check to see if our descriptor is set in rfds.
+ */
+ }
+
+ /*
+ * Some explanation:
+ *
+ * startoffset is the offset from the beginning of the input
+ * buffer to data that is in our input buffer, but has not yet
+ * been consumed. This can be non-zero if functions like
+ * netsec_readline() leave leftover data.
+ *
+ * remaining is the remaining amount of unconsumed data in the input
+ * buffer.
+ *
+ * end is a pointer to the end of the valid data + 1; it's where
+ * the next read should go.
+ */
+
+ startoffset = nsc->ns_inptr - nsc->ns_inbuffer;
+ remaining = nsc->ns_inbufsize - (startoffset + nsc->ns_inbuflen);
+ end = nsc->ns_inptr + nsc->ns_inbuflen;
+
+ /*
+ * If we're past the halfway point in our read buffers, shuffle everything
+ * back to the beginning.
+ */
+
+ if (startoffset > nsc->ns_inbufsize / 2) {
+ memmove(nsc->ns_inbuffer, nsc->ns_inptr, nsc->ns_inbuflen);
+ nsc->ns_inptr = nsc->ns_inbuffer;
+ startoffset = 0;
+ remaining = nsc->ns_inbufsize - nsc->ns_inbuflen;
+ end = nsc->ns_inptr + nsc->ns_inbuflen;
+ }
+
+ /*
+ * If we are using TLS, then just read via the BIO. But we still
+ * use our local buffer.
+ */
+#ifdef TLS_SUPPORT
+ if (nsc->tls_active) {
+ rc = BIO_read(nsc->ssl_io, end, remaining);
+ if (rc == 0) {
+ SSL *ssl;
+ int errcode;
+
+ /*
+ * Check to see if we're supposed to retry; if so,
+ * then go back and read again.
+ */
+
+ if (BIO_should_retry(nsc->ssl_io))
+ goto retry;
+
+ /*
+ * Okay, fine. Get the real error out of the SSL context.
+ */
+
+ if (BIO_get_ssl(nsc->ssl_io, &ssl) < 1) {
+ netsec_err(errstr, "SSL_read() returned 0, but cannot "
+ "retrieve SSL context");
+ return NOTOK;
+ }
+
+ errcode = SSL_get_error(ssl, rc);
+ if (errcode == SSL_ERROR_ZERO_RETURN) {
+ netsec_err(errstr, "TLS peer closed remote connection");
+ } else {
+ netsec_err(errstr, "TLS network read failed: %s",
+ ERR_error_string(ERR_peek_last_error(), NULL));
+ }
+ if (nsc->ns_snoop)
+ ERR_print_errors_fp(stderr);
+ return NOTOK;
+ }
+ if (rc < 0) {
+ /* Definitely an error */
+ netsec_err(errstr, "Read on TLS connection failed: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ return NOTOK;
+ }
+
+ nsc->ns_inbuflen += rc;
+
+ return OK;
+ }
+#endif /* TLS_SUPPORT */
+
+ /*
+ * Okay, time to read some data. Either we're just doing it straight
+ * or we're passing it through sasl_decode() first.
+ */
+
+#ifdef CYRUS_SASL
+ if (nsc->sasl_seclayer) {
+ readbuf = nsc->sasl_tmpbuf;
+ readbufsize = nsc->sasl_maxbufsize;
+ } else
+#endif /* CYRUS_SASL */
+ {
+ readbuf = (char *) end;
+ readbufsize = remaining;
+ }
+
+ /*
+ * At this point, we should have active data on the connection (see
+ * select() above) so this read SHOULDN'T block. Hopefully.
+ */
+
+ rc = read(nsc->ns_readfd, readbuf, readbufsize);
+
+ if (rc == 0) {
+ netsec_err(errstr, "Received EOF on network read");
+ return NOTOK;
+ }
+
+ if (rc < 0) {
+ netsec_err(errstr, "Network read failed: %s", strerror(errno));
+ return NOTOK;
+ }
+
+ /*
+ * Okay, so we've had a successful read. If we are doing SASL security
+ * layers, pass this through sasl_decode(). sasl_decode() can return
+ * 0 bytes decoded; if that happens, jump back to the beginning. Otherwise
+ * we can just update our length pointer.
+ */
+
+#ifdef CYRUS_SASL
+ if (nsc->sasl_seclayer) {
+ const char *tmpout;
+ unsigned int tmpoutlen;
+
+ rc = sasl_decode(nsc->sasl_conn, nsc->sasl_tmpbuf, rc,
+ &tmpout, &tmpoutlen);
+
+ if (rc != SASL_OK) {
+ netsec_err(errstr, "Unable to decode SASL network data: %s",
+ sasl_errdetail(nsc->sasl_conn));
+ return NOTOK;
+ }
+
+ if (tmpoutlen == 0)
+ goto retry;
+
+ /*
+ * Just in case ...
+ */
+
+ if (tmpoutlen > remaining) {
+ netsec_err(errstr, "Internal error: SASL decode buffer overflow!");
+ return NOTOK;
+ }
+
+ memcpy(end, tmpout, tmpoutlen);
+
+ nsc->ns_inbuflen += tmpoutlen;
+ } else
+#endif /* CYRUS_SASL */
+ nsc->ns_inbuflen += rc;
+
+ return OK;
+}
+
+/*
+ * Write data to our network connection. Really, fill up the buffer as
+ * much as we can, and flush it out if necessary. netsec_flush() does
+ * the real work.
+ */
+
+int
+netsec_write(netsec_context *nsc, const void *buffer, size_t size,
+ char **errstr)
+{
+ const unsigned char *bufptr = buffer;
+ int rc, remaining;
+
+ /* Just in case */
+
+ if (size == 0)
+ return OK;
+
+ /*
+ * Run a loop copying in data to our local buffer; when we're done with
+ * any buffer overflows then just copy any remaining data in.
+ */
+
+ while ((int) size >= (remaining = nsc->ns_outbufsize - nsc->ns_outbuflen)) {
+ memcpy(nsc->ns_outptr, bufptr, remaining);
+
+ /*
+ * In theory I should increment outptr, but netsec_flush just resets
+ * it anyway.
+ */
+ nsc->ns_outbuflen = nsc->ns_outbufsize;
+
+ rc = netsec_flush(nsc, errstr);
+
+ if (rc != OK)
+ return NOTOK;
+
+ bufptr += remaining;
+ size -= remaining;
+ }
+
+ /*
+ * Copy any leftover data into the buffer.
+ */
+
+ if (size > 0) {
+ memcpy(nsc->ns_outptr, bufptr, size);
+ nsc->ns_outptr += size;
+ nsc->ns_outbuflen += size;
+ }
+
+ return OK;
+}
+
+/*
+ * Our network printf() routine, which really just calls netsec_vprintf().
+ */
+
+int
+netsec_printf(netsec_context *nsc, char **errstr, const char *format, ...)
+{
+ va_list ap;
+ int rc;
+
+ va_start(ap, format);
+ rc = netsec_vprintf(nsc, errstr, format, ap);
+ va_end(ap);
+
+ return rc;
+}
+
+/*
+ * Write bytes to the network using printf()-style formatting.
+ *
+ * Again, for the most part copy stuff into our buffer to be flushed
+ * out later.
+ */
+
+int
+netsec_vprintf(netsec_context *nsc, char **errstr, const char *format,
+ va_list ap)
+{
+ int rc;
+
+ /*
+ * Cheat a little. If we can fit the data into our outgoing buffer,
+ * great! If not, generate a flush and retry once.
+ */
+
+retry:
+ rc = vsnprintf((char *) nsc->ns_outptr,
+ nsc->ns_outbufsize - nsc->ns_outbuflen, format, ap);
+
+ if (rc >= (int) (nsc->ns_outbufsize - nsc->ns_outbuflen)) {
+ /*
+ * This means we have an overflow. Note that we don't actually
+ * make use of the terminating NUL, but according to the spec
+ * vsnprintf() won't write to the last byte in the string; that's
+ * why we have to use >= in the comparison above.
+ */
+ if (nsc->ns_outbuffer == nsc->ns_outptr) {
+ /*
+ * Whoops, if the buffer pointer was the same as the start of the
+ * buffer, that means we overflowed the internal buffer.
+ * At that point, just give up.
+ */
+ netsec_err(errstr, "Internal error: wanted to printf() a total of "
+ "%d bytes, but our buffer size was only %d bytes",
+ rc, nsc->ns_outbufsize);
+ return NOTOK;
+ }
+ /*
+ * Generate a flush (which may be inefficient, but hopefully
+ * it isn't) and then try again.
+ */
+ if (netsec_flush(nsc, errstr) != OK)
+ return NOTOK;
+ /*
+ * After this, outbuffer should == outptr, so we shouldn't
+ * hit this next time around.
+ */
+ goto retry;
+ }
+
+ if (nsc->ns_snoop) {
+ int outlen = rc;
+ if (outlen > 0 && nsc->ns_outptr[outlen - 1] == '\n') {
+ outlen--;
+ if (outlen > 0 && nsc->ns_outptr[outlen - 1] == '\r')
+ outlen--;
+ } else {
+ nsc->ns_snoop_noend = 1;
+ }
+ if (outlen > 0 || nsc->ns_snoop_noend == 0) {
+#ifdef CYRUS_SASL
+ if (nsc->sasl_seclayer)
+ fprintf(stderr, "(sasl-encrypted) ");
+#endif /* CYRUS_SASL */
+#ifdef TLS_SUPPORT
+ if (nsc->tls_active)
+ fprintf(stderr, "(tls-encrypted) ");
+#endif /* TLS_SUPPORT */
+ fprintf(stderr, "=> ");
+ if (nsc->ns_snoop_cb)
+ nsc->ns_snoop_cb(nsc, (char *) nsc->ns_outptr, outlen,
+ nsc->ns_snoop_context);
+ else
+ fprintf(stderr, "%.*s\n", outlen, nsc->ns_outptr);
+ } else {
+ nsc->ns_snoop_noend = 0;
+ }
+ }
+
+ nsc->ns_outptr += rc;
+ nsc->ns_outbuflen += rc;
+
+ return OK;
+}
+
+/*
+ * Flush out any buffered data in our output buffers. This routine is
+ * actually where the real network writes take place.
+ */
+
+int
+netsec_flush(netsec_context *nsc, char **errstr)
+{
+ const char *netoutbuf = (const char *) nsc->ns_outbuffer;
+ unsigned int netoutlen = nsc->ns_outbuflen;
+ int rc;
+
+ /*
+ * Small optimization
+ */
+
+ if (netoutlen == 0)
+ return OK;
+
+ /*
+ * If SASL security layers are in effect, run the data through
+ * sasl_encode() first.
+ */
+#ifdef CYRUS_SASL
+ if (nsc->sasl_seclayer) {
+ rc = sasl_encode(nsc->sasl_conn, (const char *) nsc->ns_outbuffer,
+ nsc->ns_outbuflen, &netoutbuf, &netoutlen);
+
+ if (rc != SASL_OK) {
+ netsec_err(errstr, "SASL data encoding failed: %s",
+ sasl_errdetail(nsc->sasl_conn));
+ return NOTOK;
+ }
+
+ }
+#endif /* CYRUS_SASL */
+
+ /*
+ * If TLS is active, then use those functions to write out the
+ * data.
+ */
+#ifdef TLS_SUPPORT
+ if (nsc->tls_active) {
+ if (BIO_write(nsc->ssl_io, netoutbuf, netoutlen) <= 0) {
+ netsec_err(errstr, "Error writing to TLS connection: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ return NOTOK;
+ }
+ } else
+#endif /* TLS_SUPPORT */
+ {
+ rc = write(nsc->ns_writefd, netoutbuf, netoutlen);
+
+ if (rc < 0) {
+ netsec_err(errstr, "write() failed: %s", strerror(errno));
+ return NOTOK;
+ }
+ }
+
+ nsc->ns_outptr = nsc->ns_outbuffer;
+ nsc->ns_outbuflen = 0;
+
+ return OK;
+}
+
+/*
+ * Set various SASL protocol parameters
+ */
+
+int
+netsec_set_sasl_params(netsec_context *nsc, const char *service,
+ const char *mechanism, netsec_sasl_callback callback,
+ char **errstr)
+{
+#ifdef CYRUS_SASL
+ sasl_callback_t *sasl_cbs;
+ int retval;
+
+ if (!nsc->ns_hostname) {
+ netsec_err(errstr, "Internal error: ns_hostname is NULL");
+ return NOTOK;
+ }
+
+ if (! sasl_initialized) {
+ retval = sasl_client_init(NULL);
+ if (retval != SASL_OK) {
+ netsec_err(errstr, "SASL client initialization failed: %s",
+ sasl_errstring(retval, NULL, NULL));
+ return NOTOK;
+ }
+ sasl_initialized++;
+ }
+
+ /*
+ * Allocate an array of SASL callbacks for this connection.
+ * Right now we just allocate an array of four callbacks.
+ */
+
+ sasl_cbs = mh_xmalloc(sizeof(*sasl_cbs) * 4);
+
+ sasl_cbs[0].id = SASL_CB_USER;
+ sasl_cbs[0].proc = (sasl_callback_ft) netsec_get_user;
+ sasl_cbs[0].context = nsc;
+
+ sasl_cbs[1].id = SASL_CB_AUTHNAME;
+ sasl_cbs[1].proc = (sasl_callback_ft) netsec_get_user;
+ sasl_cbs[1].context = nsc;
+
+ sasl_cbs[2].id = SASL_CB_PASS;
+ sasl_cbs[2].proc = (sasl_callback_ft) netsec_get_password;
+ sasl_cbs[2].context = nsc;
+
+ sasl_cbs[3].id = SASL_CB_LIST_END;
+ sasl_cbs[3].proc = NULL;
+ sasl_cbs[3].context = NULL;
+
+ nsc->sasl_cbs = sasl_cbs;
+
+ retval = sasl_client_new(service, nsc->ns_hostname, NULL, NULL,
+ nsc->sasl_cbs, 0, &nsc->sasl_conn);
+
+ if (retval) {
+ netsec_err(errstr, "SASL new client allocation failed: %s",
+ sasl_errstring(retval, NULL, NULL));
+ return NOTOK;
+ }
+
+ /*
+ * Set up our credentials
+ */
+
+ nsc->sasl_creds = nmh_get_credentials(nsc->ns_hostname, nsc->ns_userid);
+
+#else /* CYRUS_SASL */
+ NMH_UNUSED(service);
+ NMH_UNUSED(errstr);
+#endif /* CYRUS_SASL */
+
+ /*
+ * According to the RFC, mechanisms can only be uppercase letter, numbers,
+ * and a hyphen or underscore. So make sure we uppercase any letters
+ * in case the user passed in lowercase.
+ */
+
+ if (mechanism) {
+ char *p;
+ nsc->sasl_mech = mh_xstrdup(mechanism);
+
+ for (p = nsc->sasl_mech; *p; p++)
+ if (isascii((unsigned char) *p)) /* Leave non-ASCII lower alone. */
+ *p = toupper((unsigned char) *p);
+ }
+
+ nsc->sasl_proto_cb = callback;
+
+ return OK;
+}
+
+#ifdef CYRUS_SASL
+/*
+ * Our userid callback; return the specified username to the SASL
+ * library when asked.
+ */
+
+int netsec_get_user(void *context, int id, const char **result,
+ unsigned int *len)
+{
+ netsec_context *nsc = (netsec_context *) context;
+
+ if (! result || (id != SASL_CB_USER && id != SASL_CB_AUTHNAME))
+ return SASL_BADPARAM;
+
+ *result = nmh_cred_get_user(nsc->sasl_creds);
+
+ if (len)
+ *len = strlen(*result);
+
+ return SASL_OK;
+}
+
+/*
+ * Retrieve a password and return it to SASL
+ */
+
+static int
+netsec_get_password(sasl_conn_t *conn, void *context, int id,
+ sasl_secret_t **psecret)
+{
+ netsec_context *nsc = (netsec_context *) context;
+ const char *password;
+ int len;
+
+ NMH_UNUSED(conn);
+
+ if (! psecret || id != SASL_CB_PASS)
+ return SASL_BADPARAM;
+
+ password = nmh_cred_get_password(nsc->sasl_creds);
+
+ len = strlen(password);
+
+ /*
+ * sasl_secret_t includes 1 bytes for "data" already, so that leaves
+ * us room for a terminating NUL
+ */
+
+ *psecret = (sasl_secret_t *) malloc(sizeof(sasl_secret_t) + len);
+
+ if (! *psecret)
+ return SASL_NOMEM;
+
+ (*psecret)->len = len;
+ strcpy((char *) (*psecret)->data, password);
+
+ nsc->sasl_secret = *psecret;
+
+ return SASL_OK;
+}
+#endif /* CYRUS_SASL */
+
+/*
+ * Negotiate SASL on this connection
+ */
+
+int
+netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr)
+{
+#ifdef CYRUS_SASL
+ sasl_security_properties_t secprops;
+ const char *chosen_mech;
+ const unsigned char *saslbuf;
+ unsigned char *outbuf;
+ unsigned int saslbuflen, outbuflen;
+ sasl_ssf_t *ssf;
+ int *outbufmax;
+#endif
+#ifdef OAUTH_SUPPORT
+ unsigned char *xoauth_client_res;
+ size_t xoauth_client_res_len;
+#endif /* OAUTH_SUPPORT */
+#if defined CYRUS_SASL || defined OAUTH_SUPPORT
+ int rc;
+#endif /* CYRUS_SASL || OAUTH_SUPPORT */
+
+ /*
+ * If we've been passed a requested mechanism, check our mechanism
+ * list from the protocol. If it's not supported, return an error.
+ */
+
+ if (nsc->sasl_mech) {
+ char **str, *mlist = getcpy(mechlist);
+ int i;
+
+ str = brkstring(mlist, " ", NULL);
+
+ for (i = 0; str[i] != NULL; i++) {
+ if (strcasecmp(nsc->sasl_mech, str[i]) == 0) {
+ break;
+ }
+ }
+
+ i = (str[i] == NULL);
+
+ free(mlist);
+
+ if (i) {
+ netsec_err(errstr, "Chosen mechanism %s not supported by server",
+ nsc->sasl_mech);
+ return NOTOK;
+ }
+ }
+
+#ifdef OAUTH_SUPPORT
+ if (nsc->sasl_mech && strcasecmp(nsc->sasl_mech, "XOAUTH2") == 0) {
+ /*
+ * This should be relatively straightforward, but requires some
+ * help from the plugin. Basically, if XOAUTH2 is a success,
+ * the callback has to return success, but no output data. If
+ * there is output data, it will be assumed that it is the JSON
+ * error message.
+ */
+
+ if (! nsc->oauth_service) {
+ netsec_err(errstr, "Internal error: OAuth2 service name not given");
+ return NOTOK;
+ }
+
+ nsc->sasl_chosen_mech = mh_xstrdup(nsc->sasl_mech);
+
+ if (mh_oauth_do_xoauth(nsc->ns_userid, nsc->oauth_service,
+ &xoauth_client_res, &xoauth_client_res_len,
+ nsc->ns_snoop ? stderr : NULL) != OK) {
+ netsec_err(errstr, "Internal error: Unable to get OAuth2 "
+ "bearer token");
+ return NOTOK;
+ }
+
+ rc = nsc->sasl_proto_cb(NETSEC_SASL_START, xoauth_client_res,
+ xoauth_client_res_len, NULL, 0, errstr);
+ free(xoauth_client_res);
+
+ if (rc != OK)
+ return NOTOK;
+
+ /*
+ * Okay, we need to do a NETSEC_SASL_FINISH now. If we return
+ * success, we indicate that with no output data. But if we
+ * fail, then send a blank message and get the resulting
+ * error.
+ */
+
+ rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0, errstr);
+
+ if (rc != OK) {
+ /*
+ * We're going to assume the error here is a JSON response;
+ * we ignore it and send a blank message in response. We should
+ * then get a failure messages with a useful error. We should
+ * NOT get a success message at this point.
+ */
+ free(*errstr);
+ nsc->sasl_proto_cb(NETSEC_SASL_WRITE, NULL, 0, NULL, 0, NULL);
+ rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+ errstr);
+ if (rc == 0) {
+ netsec_err(errstr, "Unexpected success after OAuth failure!");
+ }
+ return NOTOK;
+ }
+ return OK;
+ }
+#endif /* OAUTH_SUPPORT */
+
+#ifdef CYRUS_SASL
+ /*
+ * In netsec_set_sasl_params, we've already done all of our setup with
+ * sasl_client_init() and sasl_client_new(). So time to set security
+ * properties, call sasl_client_start(), and generate the protocol
+ * messages.
+ */
+
+ memset(&secprops, 0, sizeof(secprops));
+ secprops.maxbufsize = SASL_MAXRECVBUF;
+
+ /*
+ * If we're using TLS, do not negotiate a security layer
+ */
+
+ secprops.max_ssf =
+#ifdef TLS_SUPPORT
+ nsc->tls_active ? 0 :
+#endif /* TLS_SUPPORT */
+ UINT_MAX;
+
+ rc = sasl_setprop(nsc->sasl_conn, SASL_SEC_PROPS, &secprops);
+
+ if (rc != SASL_OK) {
+ netsec_err(errstr, "SASL security property initialization failed: %s",
+ sasl_errstring(rc, NULL, NULL));
+ return NOTOK;
+ }
+
+ /*
+ * Start the actual protocol negotiation, and go through the
+ * sasl_client_step() loop (after sasl_client_start, of course).
+ */
+
+ rc = sasl_client_start(nsc->sasl_conn,
+ nsc->sasl_mech ? nsc->sasl_mech : mechlist, NULL,
+ (const char **) &saslbuf, &saslbuflen,
+ &chosen_mech);
+
+ if (rc != SASL_OK && rc != SASL_CONTINUE) {
+ netsec_err(errstr, "SASL client start failed: %s",
+ sasl_errdetail(nsc->sasl_conn));
+ return NOTOK;
+ }
+
+ nsc->sasl_chosen_mech = getcpy(chosen_mech);
+
+ if (nsc->sasl_proto_cb(NETSEC_SASL_START, saslbuf, saslbuflen, NULL, 0,
+ errstr) != OK)
+ return NOTOK;
+
+ /*
+ * We've written out our first message; enter in the step loop
+ */
+
+ while (rc == SASL_CONTINUE) {
+ /*
+ * Call our SASL callback, which will handle the details of
+ * reading data from the network.
+ */
+
+ if (nsc->sasl_proto_cb(NETSEC_SASL_READ, NULL, 0, &outbuf, &outbuflen,
+ errstr) != OK) {
+ nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, NULL);
+ return NOTOK;
+ }
+
+ rc = sasl_client_step(nsc->sasl_conn, (char *) outbuf, outbuflen, NULL,
+ (const char **) &saslbuf, &saslbuflen);
+
+ mh_xfree(outbuf);
+
+ if (rc != SASL_OK && rc != SASL_CONTINUE) {
+ netsec_err(errstr, "SASL client negotiation failed: %s",
+ sasl_errdetail(nsc->sasl_conn));
+ nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, NULL);
+ return NOTOK;
+ }
+
+ if (nsc->sasl_proto_cb(NETSEC_SASL_WRITE, saslbuf, saslbuflen,
+ NULL, 0, errstr) != OK) {
+ nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, NULL);
+ return NOTOK;
+ }
+ }
+
+ /*
+ * SASL exchanges should be complete, process the final response message
+ * from the server.
+ */
+
+ if (nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+ errstr) != OK) {
+ /*
+ * At this point we can't really send an abort since the SASL dialog
+ * has completed, so just bubble back up the error message.
+ */
+
+ return NOTOK;
+ }
+
+ /*
+ * At this point, SASL should be complete. Get a few properties
+ * from the authentication exchange.
+ */
+
+ rc = sasl_getprop(nsc->sasl_conn, SASL_SSF, (const void **) &ssf);
+
+ if (rc != SASL_OK) {
+ netsec_err(errstr, "Cannot retrieve SASL negotiated security "
+ "strength factor: %s", sasl_errstring(rc, NULL, NULL));
+ return NOTOK;
+ }
+
+ nsc->sasl_ssf = *ssf;
+
+ if (nsc->sasl_ssf > 0) {
+ rc = sasl_getprop(nsc->sasl_conn, SASL_MAXOUTBUF,
+ (const void **) &outbufmax);
+
+ if (rc != SASL_OK) {
+ netsec_err(errstr, "Cannot retrieve SASL negotiated output "
+ "buffer size: %s", sasl_errstring(rc, NULL, NULL));
+ return NOTOK;
+ }
+
+ /*
+ * If our output buffer isn't the same size as the input buffer,
+ * reallocate it and set the new size (since we won't encode any
+ * data larger than that).
+ */
+
+ nsc->sasl_maxbufsize = *outbufmax;
+
+ if (nsc->ns_outbufsize != nsc->sasl_maxbufsize) {
+ nsc->ns_outbufsize = nsc->sasl_maxbufsize;
+ nsc->ns_outbuffer = mh_xrealloc(nsc->ns_outbuffer,
+ nsc->ns_outbufsize);
+ /*
+ * There shouldn't be any data in the buffer, but for
+ * consistency's sake discard it.
+ */
+ nsc->ns_outptr = nsc->ns_outbuffer;
+ nsc->ns_outbuflen = 0;
+ }
+
+ /*
+ * Allocate a buffer to do temporary reads into, before we
+ * call sasl_decode()
+ */
+
+ nsc->sasl_tmpbuf = mh_xmalloc(nsc->sasl_maxbufsize);
+
+ /*
+ * Okay, this is a bit weird. Make sure that the input buffer
+ * is at least TWICE the size of the max buffer size. That's
+ * because if we're consuming data but want to extend the current
+ * buffer, we want to be sure there's room for another full buffer's
+ * worth of data.
+ */
+
+ if (nsc->ns_inbufsize < nsc->sasl_maxbufsize * 2) {
+ size_t offset = nsc->ns_inptr - nsc->ns_inbuffer;
+ nsc->ns_inbufsize = nsc->sasl_maxbufsize * 2;
+ nsc->ns_inbuffer = mh_xrealloc(nsc->ns_inbuffer, nsc->ns_inbufsize);
+ nsc->ns_inptr = nsc->ns_inbuffer + offset;
+ }
+
+ nsc->sasl_seclayer = 1;
+ }
+
+ return OK;
+#else
+ /*
+ * If we're at this point, then either we have NEITHER OAuth2 or
+ * Cyrus-SASL compiled in, or have OAuth2 but didn't give the XOAUTH2
+ * mechanism on the command line.
+ */
+
+ if (! nsc->sasl_mech)
+ netsec_err(errstr, "SASL library support not available; please "
+ "specify a SASL mechanism to use");
+ else
+ netsec_err(errstr, "No support for the %s SASL mechanism",
+ nsc->sasl_mech);
+
+ return NOTOK;
+#endif /* CYRUS_SASL */
+}
+
+/*
+ * Retrieve our chosen SASL mechanism
+ */
+
+char *
+netsec_get_sasl_mechanism(netsec_context *nsc)
+{
+ return nsc->sasl_chosen_mech;
+}
+
+/*
+ * Set an OAuth2 service name, if we support it.
+ */
+
+int
+netsec_set_oauth_service(netsec_context *nsc, const char *service)
+{
+#ifdef OAUTH_SUPPORT
+ nsc->oauth_service = getcpy(service);
+ return OK;
+#else /* OAUTH_SUPPORT */
+ NMH_UNUSED(nsc);
+ NMH_UNUSED(service);
+ return NOTOK;
+#endif /* OAUTH_SUPPORT */
+}
+
+/*
+ * Initialize (and enable) TLS for this connection
+ */
+
+int
+netsec_set_tls(netsec_context *nsc, int tls, int noverify, char **errstr)
+{
+#ifdef TLS_SUPPORT
+ if (tls) {
+ SSL *ssl;
+ BIO *rbio, *wbio, *ssl_bio;
+
+ if (! tls_initialized) {
+ SSL_library_init();
+ SSL_load_error_strings();
+
+ /*
+ * Create the SSL context; this has the properties for all
+ * SSL connections (we are only doing one), though. Make sure
+ * we only support secure (known as of now) TLS protocols.
+ */
+
+ sslctx = SSL_CTX_new(SSLv23_client_method());
+
+ if (! sslctx) {
+ netsec_err(errstr, "Unable to initialize OpenSSL context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ return NOTOK;
+ }
+
+ SSL_CTX_set_options(sslctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
+ SSL_OP_NO_TLSv1);
+
+ if (!SSL_CTX_set_default_verify_paths(sslctx)) {
+ netsec_err(errstr, "Unable to set default certificate "
+ "verification paths: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ return NOTOK;
+ }
+
+ tls_initialized++;
+ }
+
+ if (nsc->ns_readfd == -1 || nsc->ns_writefd == -1) {
+ netsec_err(errstr, "Invalid file descriptor in netsec context");
+ return NOTOK;
+ }
+
+ /*
+ * Create the SSL structure which holds the data for a single
+ * TLS connection.
+ */
+
+ ssl = SSL_new(sslctx);
+
+ if (! ssl) {
+ netsec_err(errstr, "Unable to create SSL connection: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ return NOTOK;
+ }
+
+ /*
+ * Never bother us, since we are using blocking sockets.
+ */
+
+ SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
+
+ /*
+ * This is a bit weird, so pay attention.
+ *
+ * We create a socket BIO, and bind it to our SSL connection.
+ * That means reads and writes to the SSL connection will use our
+ * supplied socket.
+ *
+ * Then we create an SSL BIO, and assign our current SSL connection
+ * to it. This is done so our code stays simple if we want to use
+ * any buffering BIOs (right now we do our own buffering).
+ * So the chain looks like:
+ *
+ * SSL BIO -> socket BIO.
+ */
+
+ rbio = BIO_new_socket(nsc->ns_readfd, BIO_CLOSE);
+
+ if (! rbio) {
+ netsec_err(errstr, "Unable to create a read socket BIO: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ SSL_free(ssl);
+ return NOTOK;
+ }
+
+ wbio = BIO_new_socket(nsc->ns_writefd, BIO_CLOSE);
+
+ if (! wbio) {
+ netsec_err(errstr, "Unable to create a write socket BIO: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ SSL_free(ssl);
+ BIO_free(rbio);
+ return NOTOK;
+ }
+
+ SSL_set_bio(ssl, rbio, wbio);
+ SSL_set_connect_state(ssl);
+
+ /*
+ * If noverify is NOT set, then do certificate validation.
+ * Turning on SSL_VERIFY_PEER will verify the certificate chain
+ * against locally stored root certificates (the locations are
+ * set using SSL_CTX_set_default_verify_paths()), and we put
+ * the hostname in the X509 verification parameters so the OpenSSL
+ * code will verify that the hostname appears in the server
+ * certificate.
+ */
+
+ if (! noverify) {
+#ifdef HAVE_X509_VERIFY_PARAM_SET1_HOST
+ X509_VERIFY_PARAM *param;
+#endif /* HAVE_X509_VERIFY_PARAM_SET1_HOST */
+
+ SSL_set_verify(ssl, SSL_VERIFY_PEER, NULL);
+ if (! nsc->ns_hostname) {
+ netsec_err(errstr, "Internal error: hostname not set and "
+ "certification verification enabled");
+ SSL_free(ssl);
+ return NOTOK;
+ }
+
+#ifdef HAVE_X509_VERIFY_PARAM_SET1_HOST
+ param = SSL_get0_param(ssl);
+
+ if (! X509_VERIFY_PARAM_set1_host(param, nsc->ns_hostname, 0)) {
+ netsec_err(errstr, "Unable to add hostname %s to cert "
+ "verification parameters: %s", nsc->ns_hostname,
+ ERR_error_string(ERR_get_error(), NULL));
+ SSL_free(ssl);
+ return NOTOK;
+ }
+#endif /* HAVE_X509_VERIFY_PARAM_SET1_HOST */
+ }
+
+ ssl_bio = BIO_new(BIO_f_ssl());
+
+ if (! ssl_bio) {
+ netsec_err(errstr, "Unable to create a SSL BIO: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ SSL_free(ssl);
+ return NOTOK;
+ }
+
+ BIO_set_ssl(ssl_bio, ssl, BIO_CLOSE);
+ nsc->ssl_io = ssl_bio;
+
+ /*
+ * Since SSL now owns these file descriptors, have it handle the
+ * closing of them instead of netsec_shutdown().
+ */
+
+ nsc->ns_noclose = 1;
+
+ return OK;
+ }
+ BIO_free_all(nsc->ssl_io);
+ nsc->ssl_io = NULL;
+
+#else /* TLS_SUPPORT */
+ NMH_UNUSED(nsc);
+ NMH_UNUSED(noverify);
+
+ if (tls) {
+ netsec_err(errstr, "TLS is not supported");
+ return NOTOK;
+ }
+#endif /* TLS_SUPPORT */
+
+ return OK;
+}
+
+/*
+ * Start TLS negotiation on this connection
+ */
+
+int
+netsec_negotiate_tls(netsec_context *nsc, char **errstr)
+{
+#ifdef TLS_SUPPORT
+ if (! nsc->ssl_io) {
+ netsec_err(errstr, "TLS has not been configured for this connection");
+ return NOTOK;
+ }
+
+ if (BIO_do_handshake(nsc->ssl_io) < 1) {
+ unsigned long errcode = ERR_get_error();
+
+ /*
+ * Print a more detailed message if it was certificate verification
+ * failure.
+ */
+
+ if (ERR_GET_LIB(errcode) == ERR_LIB_SSL &&
+ ERR_GET_REASON(errcode) == SSL_R_CERTIFICATE_VERIFY_FAILED) {
+ SSL *ssl;
+
+ if (BIO_get_ssl(nsc->ssl_io, &ssl) < 1) {
+ netsec_err(errstr, "Certificate verification failed, but "
+ "cannot retrieve SSL handle: %s",
+ ERR_error_string(errcode, NULL));
+ } else {
+ netsec_err(errstr, "Server certificate verification failed: %s",
+ X509_verify_cert_error_string(
+ SSL_get_verify_result(ssl)));
+ }
+ } else {
+ netsec_err(errstr, "TLS negotiation failed: %s",
+ ERR_error_string(errcode, NULL));
+ }
+
+ /*
+ * Because negotiation failed, shut down TLS so we don't get any
+ * garbage on the connection. Because of weirdness with SSL_shutdown,
+ * we end up calling it twice: once explicitly, once as part of
+ * BIO_free_all().
+ */
+
+ BIO_ssl_shutdown(nsc->ssl_io);
+ BIO_free_all(nsc->ssl_io);
+ nsc->ssl_io = NULL;
+
+ return NOTOK;
+ }
+
+ if (nsc->ns_snoop) {
+ SSL *ssl;
+
+ if (BIO_get_ssl(nsc->ssl_io, &ssl) < 1) {
+ fprintf(stderr, "WARNING: cannot determine SSL ciphers\n");
+ } else {
+ const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl);
+ fprintf(stderr, "TLS negotiation successful: %s(%d) %s\n",
+ SSL_CIPHER_get_name(cipher),
+ SSL_CIPHER_get_bits(cipher, NULL),
+ SSL_CIPHER_get_version(cipher));
+ SSL_SESSION_print_fp(stderr, SSL_get_session(ssl));
+ }
+ }
+
+ nsc->tls_active = 1;
+
+ return OK;
+#else /* TLS_SUPPORT */
+ NMH_UNUSED(nsc);
+ netsec_err(errstr, "TLS not supported");
+
+ return NOTOK;
+#endif /* TLS_SUPPORT */
+}
+
+/*
+ * Generate an (allocated) error string
+ */
+
+void
+netsec_err(char **errstr, const char *fmt, ...)
+{
+ va_list ap;
+ size_t errbufsize;
+ char *errbuf = NULL;
+ int rc = 127;
+
+ if (! errstr)
+ return;
+
+ do {
+ errbufsize = rc + 1;
+ errbuf = mh_xrealloc(errbuf, errbufsize);
+ va_start(ap, fmt);
+ rc = vsnprintf(errbuf, errbufsize, fmt, ap);
+ va_end(ap);
+ } while (rc >= (int) errbufsize);
+
+ *errstr = errbuf;
+}