X-Git-Url: https://diplodocus.org/git/nmh/blobdiff_plain/90edb255effd0d29d94e662ca5bf3e9eda7ed122..8f460dd07d9ededc2c2b2a8dc99f025f494716f9:/sbr/netsec.c diff --git a/sbr/netsec.c b/sbr/netsec.c index 990b2318..ab05cf8e 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 @@ -56,12 +54,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 */ 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 */ @@ -77,7 +77,6 @@ struct _netsec_context { 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 */ sasl_callback_t *sasl_cbs; /* Callbacks used by SASL */ @@ -142,11 +141,13 @@ netsec_init(void) 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); @@ -164,7 +165,6 @@ netsec_init(void) #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; @@ -182,33 +182,24 @@ 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); - if (nsc->sasl_chosen_mech) - free(nsc->sasl_chosen_mech); + 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 - if (nsc->oauth_service) - free(nsc->oauth_service); + mh_xfree(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); + mh_xfree(nsc->sasl_cbs); if (nsc->sasl_creds) nmh_credentials_free(nsc->sasl_creds); if (nsc->sasl_secret) { @@ -217,8 +208,7 @@ netsec_shutdown(netsec_context *nsc, int closeflag) } free(nsc->sasl_secret); } - if (nsc->sasl_tmpbuf) - free(nsc->sasl_tmpbuf); + mh_xfree(nsc->sasl_tmpbuf); #endif /* CYRUS_SASL */ #ifdef TLS_SUPPORT if (nsc->ssl_io) @@ -229,7 +219,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) @@ -260,6 +250,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 */ @@ -392,7 +392,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); @@ -627,7 +627,8 @@ retry: 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)); @@ -826,19 +827,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; } + /* + * 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) { @@ -946,14 +946,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, + 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) { @@ -989,8 +994,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", @@ -998,23 +1003,20 @@ netsec_set_sasl_params(netsec_context *nsc, const char *hostname, return NOTOK; } - nsc->sasl_hostname = mh_xstrdup(hostname); - /* * Set up our credentials */ - nsc->sasl_creds = nmh_get_credentials(nsc->sasl_hostname, nsc->ns_userid); + nsc->sasl_creds = nmh_get_credentials(nsc->ns_hostname, nsc->ns_userid); #else /* CYRUS_SASL */ - NMH_UNUSED(hostname); 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. */ @@ -1023,7 +1025,7 @@ netsec_set_sasl_params(netsec_context *nsc, const char *hostname, nsc->sasl_mech = mh_xstrdup(mechanism); for (p = nsc->sasl_mech; *p; p++) - if (isascii((unsigned char) *p)) /* Just in case */ + if (isascii((unsigned char) *p)) /* Leave non-ASCII lower alone. */ *p = toupper((unsigned char) *p); } @@ -1277,8 +1279,7 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr) rc = sasl_client_step(nsc->sasl_conn, (char *) outbuf, outbuflen, NULL, (const char **) &saslbuf, &saslbuflen); - if (outbuf) - free(outbuf); + mh_xfree(outbuf); if (rc != SASL_OK && rc != SASL_CONTINUE) { netsec_err(errstr, "SASL client negotiation failed: %s", @@ -1430,12 +1431,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(); @@ -1458,6 +1459,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++; } @@ -1500,7 +1508,7 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr) * 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", @@ -1509,7 +1517,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", @@ -1522,6 +1530,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) { @@ -1534,19 +1578,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; } /* @@ -1563,8 +1617,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(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; } @@ -1579,6 +1667,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)); } } @@ -1586,6 +1675,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;