changeset: 6960:5a04f3797f03 user: Matthias Andree <matthias.and...@gmx.de> date: Tue Mar 07 18:26:04 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/5a04f3797f03
Add $ssl_verify_partial_chains option for OpenSSL. (closes #3916) The reworked OpenSSL certificate validation took away a "feature" of the previous implementation: the ability to reject a node in the chain and yet continue to the next node. If this new option is set to 'yes', enables OpenSSL's X509_V_FLAG_PARTIAL_CHAIN flag to reinstate the functionality and permit to use a non-root certificate as the trust anchor. This option is only available if OpenSSL offers the X509_V_FLAG_PARTIAL_CHAIN macro, which should be the case as of 1.0.2b or later. Code written by Kevin McCarthy and Matthias Andree. changeset: 6961:4cb6408b5fef user: Kevin McCarthy <ke...@8t8.us> date: Tue Mar 07 18:26:06 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/4cb6408b5fef Move the OpenSSL partial chain support check inside configure.ac. (see #3916) Instead of directly checking whether X509_V_FLAG_PARTIAL_CHAIN is defined everywhere, do it once inside configure. This will allow better support in the future if the test needs to change. changeset: 6962:2a0d3c4a9b0f user: Kevin McCarthy <ke...@8t8.us> date: Tue Mar 07 18:26:07 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/2a0d3c4a9b0f Don't allow storing duplicate certs for OpenSSL interactive prompt. (closes #3914) Check to make sure the certificate is not already in the $certificate_file before offering the (a)ccept always option. To allow a cert with a new validity timespan to be added to the file, check the expiration dates when comparing certificates in the certficate file. diffs (443 lines): diff -r daa9111c1f42 -r 2a0d3c4a9b0f configure.ac --- a/configure.ac Sun Mar 05 15:26:03 2017 -0800 +++ b/configure.ac Tue Mar 07 18:26:07 2017 -0800 @@ -710,6 +710,11 @@ AC_CHECK_DECLS([SSL_set_mode, SSL_MODE_AUTO_RETRY],, AC_MSG_ERROR([Unable to find decent SSL header]), [[#include <openssl/ssl.h>]]) + AC_CHECK_DECL([X509_V_FLAG_PARTIAL_CHAIN], + AC_DEFINE(HAVE_SSL_PARTIAL_CHAIN,1,[ Define if OpenSSL supports partial chains. ]), + , + [[#include <openssl/x509_vfy.h>]]) + AC_DEFINE(USE_SSL,1,[ Define if you want support for SSL. ]) AC_DEFINE(USE_SSL_OPENSSL,1,[ Define if you want support for SSL via OpenSSL. ]) LIBS="$saved_LIBS" diff -r daa9111c1f42 -r 2a0d3c4a9b0f doc/makedoc-defs.h --- a/doc/makedoc-defs.h Sun Mar 05 15:26:03 2017 -0800 +++ b/doc/makedoc-defs.h Tue Mar 07 18:26:07 2017 -0800 @@ -19,6 +19,9 @@ # ifndef USE_SSL_OPENSSL # define USE_SSL_OPENSSL # endif +# ifndef HAVE_SSL_PARTIAL_CHAIN +# define HAVE_SSL_PARTIAL_CHAIN +# endif # ifndef USE_SSL_GNUTLS # define USE_SSL_GNUTLS # endif diff -r daa9111c1f42 -r 2a0d3c4a9b0f init.h --- a/init.h Sun Mar 05 15:26:03 2017 -0800 +++ b/init.h Tue Mar 07 18:26:07 2017 -0800 @@ -78,7 +78,6 @@ }; #define UL (unsigned long) - #endif /* _MAKEDOC */ #ifndef ISPELL @@ -3377,6 +3376,24 @@ ** URL. You should only unset this for particular known hosts, using ** the \fC$<account-hook>\fP function. */ +# ifdef USE_SSL_OPENSSL +# ifdef HAVE_SSL_PARTIAL_CHAIN + { "ssl_verify_partial_chains", DT_BOOL, R_NONE, OPTSSLVERIFYPARTIAL, 0 }, + /* + ** .pp + ** This option should not be changed from the default unless you understand + ** what you are doing. + ** .pp + ** Setting this variable to \fIyes\fP will permit verifying partial + ** certification chains, i. e. a certificate chain where not the root, + ** but an intermediate certificate CA, or the host certificate, are + ** marked trusted (in $$certificate_file), without marking the root + ** signing CA as trusted. + ** .pp + ** (OpenSSL 1.0.2b and newer only). + */ +# endif /* defined HAVE_SSL_PARTIAL_CHAIN */ +# endif /* defined USE_SSL_OPENSSL */ { "ssl_ciphers", DT_STR, R_NONE, UL &SslCiphers, UL 0 }, /* ** .pp diff -r daa9111c1f42 -r 2a0d3c4a9b0f mutt.h --- a/mutt.h Sun Mar 05 15:26:03 2017 -0800 +++ b/mutt.h Tue Mar 07 18:26:07 2017 -0800 @@ -396,6 +396,9 @@ OPTSSLFORCETLS, OPTSSLVERIFYDATES, OPTSSLVERIFYHOST, +# if defined(USE_SSL_OPENSSL) && defined(HAVE_SSL_PARTIAL_CHAIN) + OPTSSLVERIFYPARTIAL, +# endif /* USE_SSL_OPENSSL */ #endif /* defined(USE_SSL) */ OPTIMPLICITAUTOVIEW, OPTINCLUDEONLYFIRST, diff -r daa9111c1f42 -r 2a0d3c4a9b0f mutt_ssl.c --- a/mutt_ssl.c Sun Mar 05 15:26:03 2017 -0800 +++ b/mutt_ssl.c Tue Mar 07 18:26:07 2017 -0800 @@ -23,6 +23,7 @@ #include <openssl/ssl.h> #include <openssl/x509.h> #include <openssl/x509v3.h> +#include <openssl/x509_vfy.h> #include <openssl/err.h> #include <openssl/rand.h> #include <openssl/evp.h> @@ -57,6 +58,12 @@ /* index for storing hostname as application specific data in SSL structure */ static int HostExDataIndex = -1; + +/* Index for storing the "skip mode" state in SSL structure. When the + * user skips a certificate in the chain, the stored value will be + * non-null. */ +static int SkipModeExDataIndex = -1; + /* keep a handle on accepted certificates in case we want to * open up another connection to the same server in this session */ static STACK_OF(X509) *SslSessionCerts = NULL; @@ -82,7 +89,7 @@ static void ssl_dprint_err_stack (void); static int ssl_cache_trusted_cert (X509 *cert); static int ssl_verify_callback (int preverify_ok, X509_STORE_CTX *ctx); -static int interactive_check_cert (X509 *cert, int idx, int len); +static int interactive_check_cert (X509 *cert, int idx, int len, SSL *ssl); 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 (CONNECTION *conn, sslsockdata*); @@ -136,6 +143,35 @@ return rv; } +static int ssl_set_verify_partial (SSL_CTX *ctx) +{ + int rv = 0; +#ifdef HAVE_SSL_PARTIAL_CHAIN + X509_VERIFY_PARAM *param; + + if (option (OPTSSLVERIFYPARTIAL)) + { + param = X509_VERIFY_PARAM_new(); + if (param) + { + X509_VERIFY_PARAM_set_flags(param, X509_V_FLAG_PARTIAL_CHAIN); + if (0 == SSL_CTX_set1_param(ctx, param)) + { + dprint (2, (debugfile, "ssl_set_verify_partial: SSL_CTX_set1_param() failed.")); + rv = -1; + } + X509_VERIFY_PARAM_free(param); + } + else + { + dprint (2, (debugfile, "ssl_set_verify_partial: X509_VERIFY_PARAM_new() failed.")); + rv = -1; + } + } +#endif + return rv; +} + /* 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) @@ -206,6 +242,12 @@ } } + if (ssl_set_verify_partial (ssldata->ctx)) + { + mutt_error (_("Warning: error enabling ssl_verify_partial_chains")); + mutt_sleep (2); + } + if (! (ssldata->ssl = SSL_new (ssldata->ctx))) { dprint (1, (debugfile, "mutt_ssl_starttls: Error allocating SSL\n")); @@ -453,6 +495,12 @@ SSL_CTX_set_cipher_list (data->ctx, SslCiphers); } + if (ssl_set_verify_partial (data->ctx)) + { + mutt_error (_("Warning: error enabling ssl_verify_partial_chains")); + mutt_sleep (2); + } + data->ssl = SSL_new (data->ctx); SSL_set_fd (data->ssl, conn->fd); @@ -489,6 +537,18 @@ return -1; } + if ((SkipModeExDataIndex = SSL_get_ex_new_index (0, "skip", NULL, NULL, NULL)) == -1) + { + dprint (1, (debugfile, "failed to get index for application specific data\n")); + return -1; + } + + if (! SSL_set_ex_data (ssldata->ssl, SkipModeExDataIndex, NULL)) + { + dprint (1, (debugfile, "failed to save skip mode in SSL structure\n")); + return -1; + } + SSL_set_verify (ssldata->ssl, SSL_VERIFY_PEER, ssl_verify_callback); SSL_set_mode (ssldata->ssl, SSL_MODE_AUTO_RETRY); ERR_clear_error (); @@ -735,7 +795,36 @@ return 0; } -static int check_certificate_by_digest (X509 *peercert) +static int check_certificate_expiration (X509 *peercert, int silent) +{ + if (option (OPTSSLVERIFYDATES) != MUTT_NO) + { + if (X509_cmp_current_time (X509_get_notBefore (peercert)) >= 0) + { + if (!silent) + { + dprint (2, (debugfile, "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) + { + if (!silent) + { + dprint (2, (debugfile, "Server certificate has expired\n")); + mutt_error (_("Server certificate has expired")); + mutt_sleep (2); + } + return 0; + } + } + + return 1; +} + +static int check_certificate_file (X509 *peercert) { unsigned char peermd[EVP_MAX_MD_SIZE]; unsigned int peermdlen; @@ -743,24 +832,8 @@ int pass = 0; FILE *fp; - /* expiration check */ - if (option (OPTSSLVERIFYDATES) != MUTT_NO) - { - if (X509_cmp_current_time (X509_get_notBefore (peercert)) >= 0) - { - dprint (2, (debugfile, "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) - { - dprint (2, (debugfile, "Server certificate has expired\n")); - mutt_error (_("Server certificate has expired")); - mutt_sleep (2); - return 0; - } - } + if (!SslCertFile) + return 0; if ((fp = fopen (SslCertFile, "rt")) == NULL) return 0; @@ -773,10 +846,12 @@ while (PEM_read_X509 (fp, &cert, NULL, NULL) != NULL) { - pass = compare_certificates (cert, peercert, peermd, peermdlen) ? 0 : 1; - - if (pass) + if ((compare_certificates (cert, peercert, peermd, peermdlen) == 0) && + check_certificate_expiration (cert, 1)) + { + pass = 1; break; + } } /* PEM_read_X509 sets an error on eof */ if (!pass) @@ -787,6 +862,12 @@ return pass; } +static int check_certificate_by_digest (X509 *peercert) +{ + return check_certificate_expiration (peercert, 0) && + check_certificate_file (peercert); +} + /* port to mutt from msmtp's tls.c */ static int hostname_match (const char *hostname, const char *certname) { @@ -947,6 +1028,7 @@ int len, pos; X509 *cert; SSL *ssl; + int skip_mode; if (! (ssl = X509_STORE_CTX_get_ex_data (ctx, SSL_get_ex_data_X509_STORE_CTX_idx ()))) { @@ -959,18 +1041,28 @@ return 0; } + /* This is true when a previous entry in the certificate chain did + * not verify and the user manually chose to skip it via the + * $ssl_verify_partial_chains option. + * In this case, all following certificates need to be treated as non-verified + * until one is actually verified. + */ + skip_mode = (SSL_get_ex_data (ssl, SkipModeExDataIndex) != NULL); + cert = X509_STORE_CTX_get_current_cert (ctx); pos = X509_STORE_CTX_get_error_depth (ctx); len = sk_X509_num (X509_STORE_CTX_get_chain (ctx)); - dprint (1, (debugfile, "ssl_verify_callback: checking cert chain entry %s (preverify: %d)\n", - X509_NAME_oneline (X509_get_subject_name (cert), - buf, sizeof (buf)), preverify_ok)); + dprint (1, (debugfile, + "ssl_verify_callback: checking cert chain entry %s (preverify: %d skipmode: %d)\n", + X509_NAME_oneline (X509_get_subject_name (cert), buf, sizeof (buf)), + preverify_ok, skip_mode)); /* check session cache first */ if (check_certificate_cache (cert)) { dprint (2, (debugfile, "ssl_verify_callback: using cached certificate\n")); + SSL_set_ex_data (ssl, SkipModeExDataIndex, NULL); return 1; } @@ -982,17 +1074,18 @@ { mutt_error (_("Certificate host check failed: %s"), buf); mutt_sleep (2); - return interactive_check_cert (cert, pos, len); + return interactive_check_cert (cert, pos, len, ssl); } dprint (2, (debugfile, "ssl_verify_callback: hostname check passed\n")); } - if (!preverify_ok) + if (!preverify_ok || skip_mode) { /* automatic check from user's database */ if (SslCertFile && check_certificate_by_digest (cert)) { dprint (2, (debugfile, "ssl_verify_callback: digest check passed\n")); + SSL_set_ex_data (ssl, SkipModeExDataIndex, NULL); return 1; } @@ -1006,14 +1099,14 @@ } #endif - /* prompt user */ - return interactive_check_cert (cert, pos, len); + /* prompt user */ + return interactive_check_cert (cert, pos, len, ssl); } return 1; } -static int interactive_check_cert (X509 *cert, int idx, int len) +static int interactive_check_cert (X509 *cert, int idx, int len, SSL *ssl) { static const int part[] = { NID_commonName, /* CN */ @@ -1032,6 +1125,7 @@ int done, row, i; unsigned u; FILE *fp; + int allow_always = 0, allow_skip = 0; menu->max = mutt_array_size (part) * 2 + 10; menu->dialog = (char **) safe_calloc (1, menu->max * sizeof (char *)); @@ -1073,18 +1167,37 @@ _("SSL Certificate check (certificate %d of %d in chain)"), len - idx, len); menu->title = title; - if (SslCertFile - && (option (OPTSSLVERIFYDATES) == MUTT_NO - || (X509_cmp_current_time (X509_get_notAfter (cert)) >= 0 - && X509_cmp_current_time (X509_get_notBefore (cert)) < 0))) + + /* The leaf/host certificate can't be skipped. */ +#ifdef HAVE_SSL_PARTIAL_CHAIN + if ((idx != 0) && + (option (OPTSSLVERIFYPARTIAL))) + allow_skip = 1; +#endif + + /* L10N: + * These four letters correspond to the choices in the next four strings: + * (r)eject, accept (o)nce, (a)ccept always, (s)kip. + * These prompts are the interactive certificate confirmation prompts for + * an OpenSSL connection. + */ + menu->keys = _("roas"); + if (SslCertFile && + check_certificate_expiration (cert, 1) && + !check_certificate_file (cert)) { - menu->prompt = _("(r)eject, accept (o)nce, (a)ccept always"); - menu->keys = _("roa"); + allow_always = 1; + if (allow_skip) + menu->prompt = _("(r)eject, accept (o)nce, (a)ccept always, (s)kip"); + else + menu->prompt = _("(r)eject, accept (o)nce, (a)ccept always"); } else { - menu->prompt = _("(r)eject, accept (o)nce"); - menu->keys = _("ro"); + if (allow_skip) + menu->prompt = _("(r)eject, accept (o)nce, (s)kip"); + else + menu->prompt = _("(r)eject, accept (o)nce"); } helpstr[0] = '\0'; @@ -1106,6 +1219,8 @@ done = 1; break; case OP_MAX + 3: /* accept always */ + if (!allow_always) + break; done = 0; if ((fp = fopen (SslCertFile, "a"))) { @@ -1126,8 +1241,15 @@ /* fall through */ case OP_MAX + 2: /* accept once */ done = 2; + SSL_set_ex_data (ssl, SkipModeExDataIndex, NULL); ssl_cache_trusted_cert (cert); break; + case OP_MAX + 4: /* skip */ + if (!allow_skip) + break; + done = 2; + SSL_set_ex_data (ssl, SkipModeExDataIndex, &SkipModeExDataIndex); + break; } } unset_option(OPTIGNOREMACROEVENTS);