X-Git-Url: http://git.madism.org/?p=apps%2Fmadmutt.git;a=blobdiff_plain;f=lib-sys%2Fmutt_ssl.c;fp=lib-sys%2Fmutt_ssl.c;h=b247377d5a60a51359d6004fd7ef43966598401e;hp=0000000000000000000000000000000000000000;hb=ccf2b75a9ed50a79c4d8e5d6235c7313fcd3719a;hpb=7b8296cfa5c33fbc73c34c4fe1ff6d7bfbaaba01 diff --git a/lib-sys/mutt_ssl.c b/lib-sys/mutt_ssl.c new file mode 100644 index 0000000..b247377 --- /dev/null +++ b/lib-sys/mutt_ssl.c @@ -0,0 +1,713 @@ +/* + * Copyright notice from original mutt: + * Copyright (C) 1999-2001 Tommi Komulainen + * + * This file is part of mutt-ng, see http://www.muttng.org/. + * It's licensed under the GNU General Public License, + * please see the file GPL in the top level source directory. + */ + +#if HAVE_CONFIG_H +# include "config.h" +#endif + +#ifdef USE_SSL + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include "mutt.h" +#include "mutt_socket.h" +#include "mutt_ssl.h" + + +#if OPENSSL_VERSION_NUMBER >= 0x00904000L +#define READ_X509_KEY(fp, key) PEM_read_X509(fp, key, NULL, NULL) +#else +#define READ_X509_KEY(fp, key) PEM_read_X509(fp, key, NULL) +#endif + +/* Just in case OpenSSL doesn't define DEVRANDOM */ +#ifndef DEVRANDOM +#define DEVRANDOM "/dev/urandom" +#endif + +/* This is ugly, but as RAND_status came in on OpenSSL version 0.9.5 + * and the code has to support older versions too, this is seemed to + * be cleaner way compared to having even uglier #ifdefs all around. + */ +#ifdef HAVE_RAND_STATUS +#define HAVE_ENTROPY() (RAND_status() == 1) +#else +static int entropy_byte_count = 0; + +/* OpenSSL fills the entropy pool from /dev/urandom if it exists */ +#define HAVE_ENTROPY() (!access(DEVRANDOM, R_OK) || entropy_byte_count >= 16) +#endif + +typedef struct _sslsockdata { + SSL_CTX *ctx; + SSL *ssl; + X509 *cert; +} sslsockdata; + +/* local prototypes */ +static int ssl_init (void); +static int add_entropy (const char *file); +static int ssl_socket_read (CONNECTION * conn, char *buf, size_t len); +static int ssl_socket_write (CONNECTION * conn, const char *buf, size_t len); +static int ssl_socket_open (CONNECTION * conn); +static int ssl_socket_close (CONNECTION * conn); +static int tls_close (CONNECTION * conn); +static int ssl_check_certificate (sslsockdata * data); +static void ssl_get_client_cert (sslsockdata * ssldata, CONNECTION * conn); +static int ssl_passwd_cb (char *buf, int size, int rwflag, void *userdata); +static int ssl_negotiate (sslsockdata *); + +/* mutt_ssl_starttls: Negotiate TLS over an already opened connection. + * TODO: Merge this code better with ssl_socket_open. */ +int mutt_ssl_starttls (CONNECTION * conn) +{ + sslsockdata *ssldata; + int maxbits; + + if (ssl_init ()) + goto bail; + + ssldata = p_new(sslsockdata, 1); + /* the ssl_use_xxx protocol options don't apply. We must use TLS in TLS. */ + if (!(ssldata->ctx = SSL_CTX_new (TLSv1_client_method ()))) { + debug_print (1, ("Error allocating SSL_CTX\n")); + goto bail_ssldata; + } + + ssl_get_client_cert (ssldata, conn); + + if (!(ssldata->ssl = SSL_new (ssldata->ctx))) { + debug_print (1, ("Error allocating SSL\n")); + goto bail_ctx; + } + + if (SSL_set_fd (ssldata->ssl, conn->fd) != 1) { + debug_print (1, ("Error setting fd\n")); + goto bail_ssl; + } + + if (ssl_negotiate (ssldata)) + goto bail_ssl; + + /* hmm. watch out if we're starting TLS over any method other than raw. */ + conn->sockdata = ssldata; + conn->conn_read = ssl_socket_read; + conn->conn_write = ssl_socket_write; + conn->conn_close = tls_close; + + conn->ssf = SSL_CIPHER_get_bits (SSL_get_current_cipher (ssldata->ssl), + &maxbits); + + return 0; + +bail_ssl: + p_delete(&ssldata->ssl); +bail_ctx: + p_delete(&ssldata->ctx); +bail_ssldata: + p_delete(&ssldata); +bail: + return -1; +} + +/* + * OpenSSL library needs to be fed with sufficient entropy. On systems + * with /dev/urandom, this is done transparently by the library itself, + * on other systems we need to fill the entropy pool ourselves. + * + * Even though only OpenSSL 0.9.5 and later will complain about the + * lack of entropy, we try to our best and fill the pool with older + * versions also. (That's the reason for the ugly #ifdefs and macros, + * otherwise I could have simply #ifdef'd the whole ssl_init funcion) + */ +static int ssl_init (void) +{ + char path[_POSIX_PATH_MAX]; + static unsigned char init_complete = 0; + + if (init_complete) + return 0; + + if (!HAVE_ENTROPY ()) { + /* load entropy from files */ + add_entropy (SslEntropyFile); + add_entropy (RAND_file_name (path, sizeof (path))); + + /* load entropy from egd sockets */ +#ifdef HAVE_RAND_EGD + add_entropy (getenv ("EGDSOCKET")); + snprintf (path, sizeof (path), "%s/.entropy", NONULL (Homedir)); + add_entropy (path); + add_entropy ("/tmp/entropy"); +#endif + + /* shuffle $RANDFILE (or ~/.rnd if unset) */ + RAND_write_file (RAND_file_name (path, sizeof (path))); + mutt_clear_error (); + if (!HAVE_ENTROPY ()) { + mutt_error (_("Failed to find enough entropy on your system")); + mutt_sleep (2); + return -1; + } + } + + /* I don't think you can do this just before reading the error. The call + * itself might clobber the last SSL error. */ + SSL_load_error_strings (); + SSL_library_init (); + init_complete = 1; + return 0; +} + +static int add_entropy (const char *file) +{ + struct stat st; + int n = -1; + + if (!file) + return 0; + + if (stat (file, &st) == -1) + return errno == ENOENT ? 0 : -1; + + mutt_message (_("Filling entropy pool: %s...\n"), file); + + /* check that the file permissions are secure */ + if (st.st_uid != getuid () || + ((st.st_mode & (S_IWGRP | S_IRGRP)) != 0) || + ((st.st_mode & (S_IWOTH | S_IROTH)) != 0)) { + mutt_error (_("%s has insecure permissions!"), file); + mutt_sleep (2); + return -1; + } + +#ifdef HAVE_RAND_EGD + n = RAND_egd (file); +#endif + if (n <= 0) + n = RAND_load_file (file, -1); + +#ifndef HAVE_RAND_STATUS + if (n > 0) + entropy_byte_count += n; +#endif + return n; +} + +static int ssl_socket_open_err (CONNECTION * conn) +{ + mutt_error (_("SSL disabled due the lack of entropy")); + mutt_sleep (2); + return -1; +} + + +int mutt_ssl_socket_setup (CONNECTION * conn) +{ + if (ssl_init () < 0) { + conn->conn_open = ssl_socket_open_err; + return -1; + } + + conn->conn_open = ssl_socket_open; + conn->conn_read = ssl_socket_read; + conn->conn_write = ssl_socket_write; + conn->conn_close = ssl_socket_close; + + return 0; +} + +static int ssl_socket_read (CONNECTION * conn, char *buf, size_t len) +{ + sslsockdata *data = conn->sockdata; + + return SSL_read (data->ssl, buf, len); +} + +static int ssl_socket_write (CONNECTION * conn, const char *buf, size_t len) +{ + sslsockdata *data = conn->sockdata; + + return SSL_write (data->ssl, buf, len); +} + +static int ssl_socket_open (CONNECTION * conn) +{ + sslsockdata *data; + int maxbits; + + if (raw_socket_open (conn) < 0) + return -1; + + data = p_new(sslsockdata, 1); + conn->sockdata = data; + + data->ctx = SSL_CTX_new (SSLv23_client_method ()); + + /* disable SSL protocols as needed */ + if (!option (OPTTLSV1)) { + SSL_CTX_set_options (data->ctx, SSL_OP_NO_TLSv1); + } + if (!option (OPTSSLV2)) { + SSL_CTX_set_options (data->ctx, SSL_OP_NO_SSLv2); + } + if (!option (OPTSSLV3)) { + SSL_CTX_set_options (data->ctx, SSL_OP_NO_SSLv3); + } + + ssl_get_client_cert (data, conn); + + data->ssl = SSL_new (data->ctx); + SSL_set_fd (data->ssl, conn->fd); + + if (ssl_negotiate (data)) { + mutt_socket_close (conn); + return -1; + } + + conn->ssf = SSL_CIPHER_get_bits (SSL_get_current_cipher (data->ssl), + &maxbits); + + return 0; +} + +/* ssl_negotiate: After SSL state has been initialised, attempt to negotiate + * SSL over the wire, including certificate checks. */ +static int ssl_negotiate (sslsockdata * ssldata) +{ + int err; + const char *errmsg; + +#if OPENSSL_VERSION_NUMBER >= 0x00906000L + /* This only exists in 0.9.6 and above. Without it we may get interrupted + * reads or writes. Bummer. */ + SSL_set_mode (ssldata->ssl, SSL_MODE_AUTO_RETRY); +#endif + + if ((err = SSL_connect (ssldata->ssl)) != 1) { + switch (SSL_get_error (ssldata->ssl, err)) { + case SSL_ERROR_SYSCALL: + errmsg = _("I/O error"); + break; + case SSL_ERROR_SSL: + errmsg = ERR_error_string (ERR_get_error (), NULL); + break; + default: + errmsg = _("unknown error"); + } + + mutt_error (_("SSL failed: %s"), errmsg); + mutt_sleep (1); + + return -1; + } + + ssldata->cert = SSL_get_peer_certificate (ssldata->ssl); + if (!ssldata->cert) { + mutt_error (_("Unable to get certificate from peer")); + mutt_sleep (1); + return -1; + } + + if (!ssl_check_certificate (ssldata)) + return -1; + + mutt_message (_("SSL connection using %s (%s)"), + SSL_get_cipher_version (ssldata->ssl), + SSL_get_cipher_name (ssldata->ssl)); + mutt_sleep (0); + + return 0; +} + +static int ssl_socket_close (CONNECTION * conn) +{ + sslsockdata *data = conn->sockdata; + + if (data) { + SSL_shutdown (data->ssl); +#if 0 + X509_free (data->cert); +#endif + SSL_free (data->ssl); + SSL_CTX_free (data->ctx); + p_delete(&conn->sockdata); + } + + return raw_socket_close (conn); +} + +static int compare_certificates (X509 *cert, X509 *peercert, + unsigned char *peermd, + unsigned int peermdlen) { + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int mdlen; + + /* Avoid CPU-intensive digest calculation if the certificates are + * not even remotely equal. + */ + if (X509_subject_name_cmp (cert, peercert) != 0 || + X509_issuer_name_cmp (cert, peercert) != 0) + return -1; + + if (!X509_digest (cert, EVP_sha1(), md, &mdlen) || peermdlen != mdlen) + return -1; + + if (memcmp(peermd, md, mdlen) != 0) + return -1; + + return 0; +} + +static int check_certificate_cache (X509 *peercert) { + unsigned char peermd[EVP_MAX_MD_SIZE]; + unsigned int peermdlen; + X509 *cert; + LIST *scert; + + if (!X509_digest (peercert, EVP_sha1(), peermd, &peermdlen)) + return 0; + + for (scert = SslSessionCerts; scert; scert = scert->next) { + cert = *(X509**)scert->data; + if (!compare_certificates (cert, peercert, peermd, peermdlen)) { + return 1; + } + } + return 0; +} + +static int tls_close (CONNECTION * conn) +{ + int rc; + + rc = ssl_socket_close (conn); + conn->conn_read = raw_socket_read; + conn->conn_write = raw_socket_write; + conn->conn_close = raw_socket_close; + + return rc; +} + +static char *x509_get_part (char *line, const char *ndx) +{ + static char ret[SHORT_STRING]; + char *c, *c2; + + m_strcpy(ret, sizeof(ret), _("Unknown")); + + c = strstr (line, ndx); + if (c) { + c += m_strlen(ndx); + c2 = strchr (c, '/'); + if (c2) + *c2 = '\0'; + m_strcpy(ret, sizeof(ret), c); + if (c2) + *c2 = '/'; + } + + return ret; +} + +static void x509_fingerprint (char *s, int l, X509 * cert) +{ + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int n; + int j; + + if (!X509_digest (cert, EVP_md5 (), md, &n)) { + m_strcpy(s, l, _("[unable to calculate]")); + } + else { + for (j = 0; j < (int) n; j++) { + char ch[8]; + + snprintf(ch, 8, "%02X%s", md[j], (j % 2 ? " " : "")); + m_strcat(s, l, ch); + } + } +} + +static char *asn1time_to_string (ASN1_UTCTIME * tm) +{ + static char buf[64]; + BIO *bio; + + m_strcpy(buf, sizeof(buf), _("[invalid date]")); + + bio = BIO_new (BIO_s_mem ()); + if (bio) { + if (ASN1_TIME_print (bio, tm)) + (void) BIO_read (bio, buf, sizeof (buf)); + BIO_free (bio); + } + + return buf; +} + +static int check_certificate_by_signer (X509 * peercert) +{ + X509_STORE_CTX xsc; + X509_STORE *ctx; + int pass = 0; + + ctx = X509_STORE_new (); + if (ctx == NULL) + return 0; + + if (option (OPTSSLSYSTEMCERTS)) { + if (X509_STORE_set_default_paths (ctx)) + pass++; + else + debug_print (2, ("X509_STORE_set_default_paths failed\n")); + } + + if (X509_STORE_load_locations (ctx, SslCertFile, NULL)) + pass++; + else + debug_print (2, ("X509_STORE_load_locations_failed\n")); + + if (pass == 0) { + /* nothing to do */ + X509_STORE_free (ctx); + return 0; + } + + X509_STORE_CTX_init (&xsc, ctx, peercert, NULL); + + pass = (X509_verify_cert (&xsc) > 0); +#ifdef DEBUG + if (!pass) { + char buf[SHORT_STRING]; + int err; + + err = X509_STORE_CTX_get_error (&xsc); + snprintf (buf, sizeof (buf), "%s (%d)", + X509_verify_cert_error_string (err), err); + debug_print (2, ("X509_verify_cert: %s\n", buf)); + } +#endif + X509_STORE_CTX_cleanup (&xsc); + X509_STORE_free (ctx); + + return pass; +} + +static int check_certificate_by_digest (X509 * peercert) +{ + unsigned char peermd[EVP_MAX_MD_SIZE]; + unsigned int peermdlen; + X509 *cert = NULL; + int pass = 0; + FILE *fp; + + /* expiration check */ + if (X509_cmp_current_time (X509_get_notBefore (peercert)) >= 0) { + debug_print (2, ("Server certificate is not yet valid\n")); + mutt_error (_("Server certificate is not yet valid")); + mutt_sleep (2); + return 0; + } + if (X509_cmp_current_time (X509_get_notAfter (peercert)) <= 0) { + debug_print (2, ("Server certificate has expired\n")); + mutt_error (_("Server certificate has expired")); + mutt_sleep (2); + return 0; + } + + if ((fp = fopen (SslCertFile, "rt")) == NULL) + return 0; + + if (!X509_digest (peercert, EVP_sha1 (), peermd, &peermdlen)) { + fclose (fp); + return 0; + } + + while ((cert = READ_X509_KEY (fp, &cert)) != NULL) { + pass = compare_certificates (cert, peercert, peermd, peermdlen) ? 0 : 1; + if (pass) + break; + } + X509_free (cert); + fclose (fp); + + return pass; +} + +static int ssl_check_certificate (sslsockdata * data) +{ + char *part[] = { "/CN=", "/Email=", "/O=", "/OU=", "/L=", "/ST=", "/C=" }; + char helpstr[SHORT_STRING]; + char buf[SHORT_STRING]; + MUTTMENU *menu; + int done, row, i; + FILE *fp; + char *name = NULL, *c; + + /* check session cache first */ + if (check_certificate_cache (data->cert)) { + debug_print (1, ("ssl_check_certificate: using cached certificate\n")); + return 1; + } + + if (check_certificate_by_signer (data->cert)) { + debug_print (1, ("signer check passed\n")); + return 1; + } + + /* automatic check from user's database */ + if (SslCertFile && check_certificate_by_digest (data->cert)) { + debug_print (1, ("digest check passed\n")); + return 1; + } + + /* interactive check from user */ + menu = mutt_new_menu (); + menu->max = 19; + menu->dialog = p_new(char *, menu->max); + for (i = 0; i < menu->max; i++) + menu->dialog[i] = p_new(char, SHORT_STRING); + + row = 0; + m_strcpy(menu->dialog[row], SHORT_STRING, + _("This certificate belongs to:")); + row++; + name = X509_NAME_oneline (X509_get_subject_name (data->cert), + buf, sizeof (buf)); + for (i = 0; i < 5; i++) { + c = x509_get_part (name, part[i]); + snprintf (menu->dialog[row++], SHORT_STRING, " %s", c); + } + + row++; + m_strcpy(menu->dialog[row], SHORT_STRING, + _("This certificate was issued by:")); + row++; + name = X509_NAME_oneline (X509_get_issuer_name (data->cert), + buf, sizeof (buf)); + for (i = 0; i < 5; i++) { + c = x509_get_part (name, part[i]); + snprintf (menu->dialog[row++], SHORT_STRING, " %s", c); + } + + row++; + snprintf (menu->dialog[row++], SHORT_STRING, "%s", + _("This certificate is valid")); + snprintf (menu->dialog[row++], SHORT_STRING, _(" from %s"), + asn1time_to_string (X509_get_notBefore (data->cert))); + snprintf (menu->dialog[row++], SHORT_STRING, _(" to %s"), + asn1time_to_string (X509_get_notAfter (data->cert))); + + row++; + buf[0] = '\0'; + x509_fingerprint (buf, sizeof (buf), data->cert); + snprintf (menu->dialog[row++], SHORT_STRING, _("Fingerprint: %s"), buf); + + menu->title = _("SSL Certificate check"); + + if (SslCertFile && X509_cmp_current_time (X509_get_notAfter (data->cert)) >= 0 + && X509_cmp_current_time (X509_get_notBefore (data->cert)) < 0) { + menu->prompt = _("(r)eject, accept (o)nce, (a)ccept always"); + menu->keys = _("roa"); + } + else { + menu->prompt = _("(r)eject, accept (o)nce"); + menu->keys = _("ro"); + } + + helpstr[0] = '\0'; + mutt_make_help (buf, sizeof (buf), _("Exit "), MENU_GENERIC, OP_EXIT); + m_strcat(helpstr, sizeof(helpstr), buf); + mutt_make_help (buf, sizeof (buf), _("Help"), MENU_GENERIC, OP_HELP); + m_strcat(helpstr, sizeof(helpstr), buf); + menu->help = helpstr; + + done = 0; + set_option (OPTUNBUFFEREDINPUT); + while (!done) { + switch (mutt_menuLoop (menu)) { + case -1: /* abort */ + case OP_MAX + 1: /* reject */ + case OP_EXIT: + done = 1; + break; + case OP_MAX + 3: /* accept always */ + done = 0; + if ((fp = fopen (SslCertFile, "a"))) { + if (PEM_write_X509 (fp, data->cert)) + done = 1; + fclose (fp); + } + if (!done) { + mutt_error (_("Warning: Couldn't save certificate")); + mutt_sleep (2); + } + else { + mutt_message (_("Certificate saved")); + mutt_sleep (0); + } + /* fall through */ + case OP_MAX + 2: /* accept once */ + done = 2; + /* keep a handle on accepted certificates in case we want to + * open up another connection to the same server in this session */ + SslSessionCerts = mutt_add_list_n (SslSessionCerts, &data->cert, + sizeof (X509 **)); + break; + } + } + unset_option (OPTUNBUFFEREDINPUT); + mutt_menuDestroy (&menu); + return (done == 2); +} + +static void ssl_get_client_cert (sslsockdata * ssldata, CONNECTION * conn) +{ + if (SslClientCert) { + debug_print (2, ("Using client certificate %s\n", SslClientCert)); + SSL_CTX_set_default_passwd_cb_userdata (ssldata->ctx, &conn->account); + SSL_CTX_set_default_passwd_cb (ssldata->ctx, ssl_passwd_cb); + SSL_CTX_use_certificate_file (ssldata->ctx, SslClientCert, + SSL_FILETYPE_PEM); + SSL_CTX_use_PrivateKey_file (ssldata->ctx, SslClientCert, + SSL_FILETYPE_PEM); + } +} + +static int ssl_passwd_cb (char *buf, int size, int rwflag, void *userdata) +{ + ACCOUNT *account = (ACCOUNT *) userdata; + + if (mutt_account_getuser (account)) + return 0; + + debug_print (2, ("getting password for %s@%s:%u\n", + account->user, account->host, account->port)); + + if (mutt_account_getpass (account)) + return 0; + + return snprintf (buf, size, "%s", account->pass); +} + +#endif /* USE_SSL */