X-Git-Url: https://diplodocus.org/git/nmh/blobdiff_plain/15c89585836a98f6340619a58a1ec0e7e1d83dfc..94187a80bd60baab4b9c4b949ad820d730578123:/sbr/netsec.c diff --git a/sbr/netsec.c b/sbr/netsec.c index 18bb97ac..ab490467 100644 --- a/sbr/netsec.c +++ b/sbr/netsec.c @@ -1,6 +1,4 @@ - -/* - * 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 @@ -14,6 +12,7 @@ #include #include #include +#include "base64.h" #ifdef CYRUS_SASL #include @@ -56,12 +55,14 @@ static SSL_CTX *sslctx = NULL; /* SSL Context */ struct _netsec_context { 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 */ + char *ns_snoop_savebuf; /* Save buffer for snoop data */ 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 */ @@ -71,18 +72,18 @@ struct _netsec_context { 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 */ + void *sasl_proto_context; /* Context to be used by SASL callback */ #ifdef OAUTH_SUPPORT char *oauth_service; /* OAuth2 service name */ #endif /* OAUTH_SUPPORT */ #ifdef CYRUS_SASL - char *sasl_hostname; /* Hostname we've connected to */ sasl_conn_t *sasl_conn; /* SASL connection context */ sasl_ssf_t sasl_ssf; /* SASL Security Strength Factor */ - netsec_sasl_callback sasl_proto_cb; /* SASL callback we use */ 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 */ - char *sasl_chosen_mech; /* Mechanism chosen by SASL */ 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 */ @@ -108,17 +109,26 @@ 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 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. * - * 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. + * 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, 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). + * 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). */ /* @@ -128,15 +138,18 @@ static int checkascii(const unsigned char *byte, size_t len); netsec_context * netsec_init(void) { - netsec_context *nsc = mh_xmalloc(sizeof(*nsc)); + netsec_context *nsc; + 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_snoop_savebuf = 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); @@ -147,16 +160,17 @@ netsec_init(void) nsc->ns_outptr = nsc->ns_outbuffer; nsc->ns_outbuflen = 0; nsc->sasl_mech = NULL; + nsc->sasl_chosen_mech = NULL; + nsc->sasl_proto_cb = NULL; + nsc->sasl_proto_context = NULL; #ifdef OAUTH_SUPPORT nsc->oauth_service = NULL; #endif /* OAUTH_SUPPORT */ #ifdef CYRUS_SASL nsc->sasl_conn = NULL; - nsc->sasl_hostname = NULL; nsc->sasl_cbs = NULL; nsc->sasl_creds = NULL; nsc->sasl_secret = NULL; - nsc->sasl_chosen_mech = NULL; nsc->sasl_ssf = 0; nsc->sasl_seclayer = 0; nsc->sasl_tmpbuf = NULL; @@ -171,47 +185,34 @@ netsec_init(void) /* * Shutdown the connection completely and free all resources. - * The connection is only closed if the flag is given. */ void -netsec_shutdown(netsec_context *nsc, int closeflag) +netsec_shutdown(netsec_context *nsc) { - if (nsc->ns_userid) - free(nsc->ns_userid); - if (nsc->ns_inbuffer) - free(nsc->ns_inbuffer); - if (nsc->ns_outbuffer) - free(nsc->ns_outbuffer); - if (nsc->sasl_mech) - free(nsc->sasl_mech); + free(nsc->ns_userid); + free(nsc->ns_hostname); + free(nsc->ns_inbuffer); + free(nsc->ns_outbuffer); + free(nsc->sasl_mech); + free(nsc->sasl_chosen_mech); + free(nsc->ns_snoop_savebuf); #ifdef OAUTH_SERVICE - if (nsc->oauth_service) - free(nsc->oauth_service); + free(nsc->oauth_service); #endif /* OAUTH_SERVICE */ #ifdef CYRUS_SASL if (nsc->sasl_conn) sasl_dispose(&nsc->sasl_conn); - if (nsc->sasl_hostname) - free(nsc->sasl_hostname); - if (nsc->sasl_cbs) - free(nsc->sasl_cbs); - if (nsc->sasl_creds) { - if (nsc->sasl_creds->password) - memset(nsc->sasl_creds->password, 0, - strlen(nsc->sasl_creds->password)); - free(nsc->sasl_creds); - } + free(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); } - if (nsc->sasl_chosen_mech) - free(nsc->sasl_chosen_mech); - if (nsc->sasl_tmpbuf) - free(nsc->sasl_tmpbuf); + free(nsc->sasl_tmpbuf); #endif /* CYRUS_SASL */ #ifdef TLS_SUPPORT if (nsc->ssl_io) @@ -222,7 +223,7 @@ netsec_shutdown(netsec_context *nsc, int closeflag) BIO_free_all(nsc->ssl_io); #endif /* TLS_SUPPORT */ - if (closeflag) { + if (! nsc->ns_noclose) { if (nsc->ns_readfd != -1) close(nsc->ns_readfd); if (nsc->ns_writefd != -1 && nsc->ns_writefd != nsc->ns_readfd) @@ -253,6 +254,16 @@ 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 */ @@ -310,7 +321,7 @@ netsec_b64_snoop_decoder(netsec_context *nsc, const char *string, size_t len, if (decodeBase64(string, &decoded, &decodedlen, 1, NULL) == OK) { /* - * Some mechanisms preoduce large binary tokens, which aren't really + * 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 @@ -385,7 +396,7 @@ netsec_read(netsec_context *nsc, void *buffer, size_t size, char **errstr) * assume here that this has something in it. */ - retlen = size > nsc->ns_inbuflen ? nsc->ns_inbuflen : size; + retlen = min(size, nsc->ns_inbuflen); memcpy(buffer, nsc->ns_inptr, retlen); @@ -410,11 +421,12 @@ netsec_read(netsec_context *nsc, void *buffer, size_t size, char **errstr) * * - Unlike every other function, we return a pointer to the * existing buffer. This pointer is valid until you call another - * read functiona again. - * - We NUL-terminated the buffer right at the end, before the terminator. + * 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. + * - If your data may contain embedded NULs, this won't work. You should + * be using netsec_read() in that case. */ char * @@ -452,8 +464,10 @@ retry: if (nsc->ns_snoop_cb) nsc->ns_snoop_cb(nsc, sptr, strlen(sptr), nsc->ns_snoop_context); - else - fprintf(stderr, "%s\n", sptr); + else { + fputs(sptr, stderr); + putc('\n', stderr); + } } return sptr; } @@ -465,7 +479,7 @@ retry: */ if (count >= nsc->ns_inbufsize / 2) { - netsec_err(errstr, "Unable to find a line terminator after %d bytes", + netsec_err(errstr, "Unable to find a line terminator after %zu bytes", count); return NULL; } @@ -508,7 +522,9 @@ netsec_fillread(netsec_context *nsc, char **errstr) 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 @@ -546,10 +562,38 @@ retry: */ } + /* + * 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. @@ -558,13 +602,39 @@ retry: if (nsc->tls_active) { rc = BIO_read(nsc->ssl_io, end, remaining); if (rc == 0) { + SSL *ssl; + int errcode; + /* - * Either EOF, or possibly an error. Either way, it was probably - * unexpected, so treat as error. + * Check to see if we're supposed to retry; if so, + * then go back and read again. */ - netsec_err(errstr, "TLS peer aborted connection"); + + 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; - } else if (rc < 0) { + } + if (rc < 0) { /* Definitely an error */ netsec_err(errstr, "Read on TLS connection failed: %s", ERR_error_string(ERR_get_error(), NULL)); @@ -650,16 +720,6 @@ retry: #endif /* CYRUS_SASL */ nsc->ns_inbuflen += rc; - /* - * 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; - } - return OK; } @@ -773,48 +833,18 @@ retry: "%d bytes, but our buffer size was only %d bytes", rc, nsc->ns_outbufsize); return NOTOK; - } else { - /* - * 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; } + /* + * 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; } nsc->ns_outptr += rc; @@ -842,6 +872,95 @@ netsec_flush(netsec_context *nsc, char **errstr) if (netoutlen == 0) return OK; + /* + * If we have snoop turned on, output the data. + * + * Note here; if we don't have a CR or LF at the end, save the data + * in ns_snoop_savebuf for later and print it next time. + */ + + if (nsc->ns_snoop) { + unsigned int snoopoutlen = netoutlen; + const char *snoopoutbuf = (const char *) nsc->ns_outbuffer; + + while (snoopoutlen > 0) { + const char *end = strpbrk(snoopoutbuf, "\r\n"); + unsigned int outlen; + + if (! end) { + if (nsc->ns_snoop_savebuf) { + nsc->ns_snoop_savebuf = mh_xrealloc(nsc->ns_snoop_savebuf, + strlen(nsc->ns_snoop_savebuf) + + snoopoutlen + 1); + strncat(nsc->ns_snoop_savebuf, snoopoutbuf, snoopoutlen); + } else { + nsc->ns_snoop_savebuf = mh_xmalloc(snoopoutlen + 1); + strncpy(nsc->ns_snoop_savebuf, snoopoutbuf, snoopoutlen); + nsc->ns_snoop_savebuf[snoopoutlen] = '\0'; + } + break; + } + + outlen = end - snoopoutbuf; + +#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) { + const char *ptr; + unsigned int cb_len = outlen; + + if (nsc->ns_snoop_savebuf) { + cb_len += strlen(nsc->ns_snoop_savebuf); + nsc->ns_snoop_savebuf = mh_xrealloc(nsc->ns_snoop_savebuf, + outlen); + ptr = nsc->ns_snoop_savebuf; + } else { + ptr = snoopoutbuf; + } + + nsc->ns_snoop_cb(nsc, ptr, cb_len, nsc->ns_snoop_context); + + if (nsc->ns_snoop_savebuf) { + free(nsc->ns_snoop_savebuf); + nsc->ns_snoop_savebuf = NULL; + } + } else { + if (nsc->ns_snoop_savebuf) { + fprintf(stderr, "%s", nsc->ns_snoop_savebuf); + free(nsc->ns_snoop_savebuf); + nsc->ns_snoop_savebuf = NULL; + } + fprintf(stderr, "%.*s\n", outlen, snoopoutbuf); + } + + /* + * Alright, hopefully any previous leftover data is done, + * and we have the current line output. Move things past the + * next CR/LF. + */ + + snoopoutlen -= outlen; + snoopoutbuf += outlen; + + if (snoopoutlen > 0 && *snoopoutbuf == '\r') { + snoopoutlen--; + snoopoutbuf++; + } + + if (snoopoutlen > 0 && *snoopoutbuf == '\n') { + snoopoutlen--; + snoopoutbuf++; + } + } + } + /* * If SASL security layers are in effect, run the data through * sasl_encode() first. @@ -893,14 +1012,19 @@ netsec_flush(netsec_context *nsc, char **errstr) */ int -netsec_set_sasl_params(netsec_context *nsc, const char *hostname, - const char *service, const char *mechanism, - netsec_sasl_callback callback, char **errstr) +netsec_set_sasl_params(netsec_context *nsc, const char *service, + const char *mechanism, netsec_sasl_callback callback, + void *context, 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) { @@ -936,8 +1060,8 @@ netsec_set_sasl_params(netsec_context *nsc, const char *hostname, nsc->sasl_cbs = sasl_cbs; - retval = sasl_client_new(service, hostname, NULL, NULL, nsc->sasl_cbs, 0, - &nsc->sasl_conn); + 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", @@ -945,30 +1069,32 @@ netsec_set_sasl_params(netsec_context *nsc, const char *hostname, 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 hypen or underscore. So make sure we uppercase any letters + * 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 = getcpy(mechanism); - - for (p = nsc->sasl_mech; *p; p++) - if (isascii((unsigned char) *p)) /* Just in case */ - *p = toupper((unsigned char) *p); + nsc->sasl_mech = mh_xstrdup(mechanism); + to_upper(nsc->sasl_mech); } nsc->sasl_proto_cb = callback; - nsc->sasl_hostname = getcpy(hostname); + nsc->sasl_proto_context = context; return OK; -#else /* CYRUS_SASL */ - netsec_err(errstr, "SASL is not supported"); - - return NOTOK; -#endif /* CYRUS_SASL */ } #ifdef CYRUS_SASL @@ -985,35 +1111,10 @@ int netsec_get_user(void *context, int id, const char **result, if (! result || (id != SASL_CB_USER && id != SASL_CB_AUTHNAME)) return SASL_BADPARAM; - if (nsc->ns_userid == NULL) { - /* - * Pass the 1 third argument to nmh_get_credentials() so that - * a defauly user if the -user switch wasn't supplied, and so - * that a default password will be supplied. That's used when - * those values really don't matter, and only with legacy/.netrc, - * i.e., with a credentials profile entry. - */ - - if (nsc->sasl_creds == NULL) { - nsc->sasl_creds = mh_xmalloc(sizeof(*nsc->sasl_creds)); - nsc->sasl_creds->user = NULL; - nsc->sasl_creds->password = NULL; - } - - if (nmh_get_credentials(nsc->sasl_hostname, nsc->ns_userid, 1, - nsc->sasl_creds) != OK) - return SASL_BADPARAM; - - if (nsc->ns_userid != nsc->sasl_creds->user) { - if (nsc->ns_userid) - free(nsc->ns_userid); - nsc->ns_userid = getcpy(nsc->sasl_creds->user); - } - } + *result = nmh_cred_get_user(nsc->sasl_creds); - *result = nsc->ns_userid; if (len) - *len = strlen(nsc->ns_userid); + *len = strlen(*result); return SASL_OK; } @@ -1027,6 +1128,7 @@ 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); @@ -1034,40 +1136,22 @@ netsec_get_password(sasl_conn_t *conn, void *context, int id, if (! psecret || id != SASL_CB_PASS) return SASL_BADPARAM; - if (nsc->sasl_creds == NULL) { - nsc->sasl_creds = mh_xmalloc(sizeof(*nsc->sasl_creds)); - nsc->sasl_creds->user = NULL; - nsc->sasl_creds->password = NULL; - } + password = nmh_cred_get_password(nsc->sasl_creds); - if (nsc->sasl_creds->password == NULL) { - /* - * Pass the 0 third argument to nmh_get_credentials() so - * that the default password isn't used. With legacy/.netrc - * credentials support, we'll only get here if the -user - * switch to send(1)/post(8) wasn't used. - */ - - if (nmh_get_credentials(nsc->sasl_hostname, nsc->ns_userid, 0, - nsc->sasl_creds) != OK) { - return SASL_BADPARAM; - } - } - - len = strlen(nsc->sasl_creds->password); + 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); + *psecret = malloc(sizeof(sasl_secret_t) + len); if (! *psecret) return SASL_NOMEM; (*psecret)->len = len; - strcpy((char *) (*psecret)->data, nsc->sasl_creds->password); + strcpy((char *) (*psecret)->data, password); nsc->sasl_secret = *psecret; @@ -1095,7 +1179,9 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) 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 @@ -1140,7 +1226,7 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) return NOTOK; } - nsc->sasl_chosen_mech = getcpy(nsc->sasl_mech); + 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, @@ -1151,7 +1237,8 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) } rc = nsc->sasl_proto_cb(NETSEC_SASL_START, xoauth_client_res, - xoauth_client_res_len, NULL, 0, errstr); + xoauth_client_res_len, NULL, 0, + nsc->sasl_proto_context, errstr); free(xoauth_client_res); if (rc != OK) @@ -1164,18 +1251,21 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) * error. */ - rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0, errstr); + rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0, + nsc->sasl_proto_context, 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 either an +OK or -ERR + * 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); + nsc->sasl_proto_cb(NETSEC_SASL_WRITE, NULL, 0, NULL, 0, + nsc->sasl_proto_context, NULL); rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0, - errstr); + nsc->sasl_proto_context, errstr); if (rc == 0) { netsec_err(errstr, "Unexpected success after OAuth failure!"); } @@ -1193,7 +1283,7 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) * messages. */ - memset(&secprops, 0, sizeof(secprops)); + ZERO(&secprops); secprops.maxbufsize = SASL_MAXRECVBUF; /* @@ -1233,7 +1323,7 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) nsc->sasl_chosen_mech = getcpy(chosen_mech); if (nsc->sasl_proto_cb(NETSEC_SASL_START, saslbuf, saslbuflen, NULL, 0, - errstr) != OK) + nsc->sasl_proto_context, errstr) != OK) return NOTOK; /* @@ -1247,27 +1337,30 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) */ 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); + nsc->sasl_proto_context, errstr) != OK) { + nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, + nsc->sasl_proto_context, NULL); return NOTOK; } rc = sasl_client_step(nsc->sasl_conn, (char *) outbuf, outbuflen, NULL, (const char **) &saslbuf, &saslbuflen); - if (outbuf) - free(outbuf); + free(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); + nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, + nsc->sasl_proto_context, 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); + NULL, 0, nsc->sasl_proto_context, + errstr) != OK) { + nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, + nsc->sasl_proto_context, NULL); return NOTOK; } } @@ -1278,7 +1371,7 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) */ if (nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0, - errstr) != OK) { + nsc->sasl_proto_context, 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. @@ -1359,7 +1452,18 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) return OK; #else - netsec_err(errstr, "SASL not supported"); + /* + * 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 */ @@ -1372,10 +1476,20 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) char * netsec_get_sasl_mechanism(netsec_context *nsc) { -#ifdef CYRUS_SASL return nsc->sasl_chosen_mech; +} + +/* + * Return the negotiated SASL strength security factor (SSF) + */ + +int +netsec_get_sasl_ssf(netsec_context *nsc) +{ +#ifdef CYRUS_SASL + return nsc->sasl_ssf; #else /* CYRUS_SASL */ - return NULL; + return 0; #endif /* CYRUS_SASL */ } @@ -1390,6 +1504,8 @@ netsec_set_oauth_service(netsec_context *nsc, const char *service) nsc->oauth_service = getcpy(service); return OK; #else /* OAUTH_SUPPORT */ + NMH_UNUSED(nsc); + NMH_UNUSED(service); return NOTOK; #endif /* OAUTH_SUPPORT */ } @@ -1399,12 +1515,12 @@ netsec_set_oauth_service(netsec_context *nsc, const char *service) */ int -netsec_set_tls(netsec_context *nsc, int tls, char **errstr) +netsec_set_tls(netsec_context *nsc, int tls, int noverify, char **errstr) { - if (tls) { #ifdef TLS_SUPPORT + if (tls) { SSL *ssl; - BIO *rbio, *wbio, *ssl_bio;; + BIO *rbio, *wbio, *ssl_bio; if (! tls_initialized) { SSL_library_init(); @@ -1427,6 +1543,13 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr) 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++; } @@ -1462,15 +1585,14 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr) * supplied socket. * * Then we create an SSL BIO, and assign our current SSL connection - * to it. We then create a buffer BIO and push it in front of our - * SSL BIO. So the chain looks like: - * - * buffer BIO -> SSL BIO -> socket BIO. + * 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: * - * So writes and reads are buffered (we mostly care about writes). + * SSL BIO -> socket BIO. */ - rbio = BIO_new_socket(nsc->ns_readfd, BIO_NOCLOSE); + rbio = BIO_new_socket(nsc->ns_readfd, BIO_CLOSE); if (! rbio) { netsec_err(errstr, "Unable to create a read socket BIO: %s", @@ -1479,7 +1601,7 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr) return NOTOK; } - wbio = BIO_new_socket(nsc->ns_writefd, BIO_NOCLOSE); + wbio = BIO_new_socket(nsc->ns_writefd, BIO_CLOSE); if (! wbio) { netsec_err(errstr, "Unable to create a write socket BIO: %s", @@ -1492,6 +1614,42 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr) 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) { @@ -1504,19 +1662,29 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr) BIO_set_ssl(ssl_bio, ssl, BIO_CLOSE); nsc->ssl_io = ssl_bio; - return OK; - } else { - BIO_free_all(nsc->ssl_io); - nsc->ssl_io = NULL; + /* + * 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 */ - netsec_err(errstr, "TLS is not supported"); + NMH_UNUSED(nsc); + NMH_UNUSED(noverify); + if (tls) { + netsec_err(errstr, "TLS is not supported"); return NOTOK; } #endif /* TLS_SUPPORT */ + + return OK; } /* @@ -1533,8 +1701,42 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr) } if (BIO_do_handshake(nsc->ssl_io) < 1) { - netsec_err(errstr, "TLS negotiation failed: %s", - ERR_error_string(ERR_get_error(), NULL)); + 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(ERR_get_error(), 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; } @@ -1549,6 +1751,7 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr) 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)); } } @@ -1556,6 +1759,7 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr) return OK; #else /* TLS_SUPPORT */ + NMH_UNUSED(nsc); netsec_err(errstr, "TLS not supported"); return NOTOK;