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);

Reply via email to