/* Copyright (C) 2001 Marco d'Itri <md@linux.it>
 * Copyright (C) 2001-2004 Andrew McDonald <andrew@mcdonald.org.uk>
 *
 *     This program is free software; you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation; either version 2 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program; if not, write to the Free Software
 *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#if HAVE_CONFIG_H
# include "config.h"
#endif

#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#ifdef HAVE_GNUTLS_OPENSSL_H
#include <gnutls/openssl.h>
#endif

#include "mutt.h"
#include "mutt_socket.h"
#include "mutt_curses.h"
#include "mutt_menu.h"
#include "mutt_ssl.h"
#include "mutt_regex.h"

/* certificate error bitmap values */
#define CERTERR_VALID       0
#define CERTERR_EXPIRED     1
#define CERTERR_NOTYETVALID (1<<1)
#define CERTERR_REVOKED     (1<<2)
#define CERTERR_NOTTRUSTED  (1<<3)
#define CERTERR_HOSTNAME    (1<<4)
#define CERTERR_SIGNERNOTCA (1<<5)
#define CERTERR_INSECUREALG (1<<6)
#define CERTERR_OTHER       (1<<7)

/* deprecated types compatibility */

#ifndef HAVE_GNUTLS_CERTIFICATE_CREDENTIALS_T
typedef gnutls_certificate_credentials gnutls_certificate_credentials_t;
#endif

#ifndef HAVE_GNUTLS_CERTIFICATE_STATUS_T
typedef gnutls_certificate_status gnutls_certificate_status_t;
#endif

#ifndef HAVE_GNUTLS_DATUM_T
typedef gnutls_datum gnutls_datum_t;
#endif

#ifndef HAVE_GNUTLS_DIGEST_ALGORITHM_T
typedef gnutls_digest_algorithm gnutls_digest_algorithm_t;
#endif

#ifndef HAVE_GNUTLS_SESSION_T
typedef gnutls_session gnutls_session_t;
#endif

#ifndef HAVE_GNUTLS_TRANSPORT_PTR_T
typedef gnutls_transport_ptr gnutls_transport_ptr_t;
#endif

#ifndef HAVE_GNUTLS_X509_CRT_T
typedef gnutls_x509_crt gnutls_x509_crt_t;
#endif


typedef struct _tlssockdata
{
  gnutls_session_t state;
  gnutls_certificate_credentials_t xcred;
} tlssockdata;

/* local prototypes */
static int tls_socket_read(CONNECTION *conn, char *buf, size_t len);
static int tls_socket_write(CONNECTION *conn, const char *buf, size_t len);
static int tls_socket_poll(CONNECTION *conn, time_t wait_secs);
static int tls_socket_open(CONNECTION *conn);
static int tls_socket_close(CONNECTION *conn);
static int tls_starttls_close(CONNECTION *conn);

static int tls_init(void);
static int tls_negotiate(CONNECTION *conn);
static int tls_check_certificate(CONNECTION *conn);
static int tls_passwd_cb(void *userdata, int attempt, const char *token_url,
                         const char *token_label,
                         unsigned int flags,
                         char *pin, size_t pin_max);


static int tls_init(void)
{
  static unsigned char init_complete = 0;
  int err;

  if (init_complete)
    return 0;

  err = gnutls_global_init();
  if (err < 0)
  {
    mutt_error("gnutls_global_init: %s", gnutls_strerror(err));
    mutt_sleep(2);
    return -1;
  }

  init_complete = 1;
  return 0;
}

int mutt_ssl_socket_setup(CONNECTION *conn)
{
  if (tls_init() < 0)
    return -1;

  conn->conn_open       = tls_socket_open;
  conn->conn_read       = tls_socket_read;
  conn->conn_write      = tls_socket_write;
  conn->conn_close      = tls_socket_close;
  conn->conn_poll       = tls_socket_poll;

  return 0;
}

static int tls_socket_read(CONNECTION *conn, char *buf, size_t len)
{
  tlssockdata *data = conn->sockdata;
  int ret;

  if (!data)
  {
    mutt_error(_("Error: no TLS socket open"));
    mutt_sleep(2);
    return -1;
  }

  do
  {
    ret = gnutls_record_recv(data->state, buf, len);
  } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);

  if (ret < 0)
  {
    mutt_error("tls_socket_read (%s)", gnutls_strerror(ret));
    mutt_sleep(4);
    return -1;
  }

  return ret;
}

static int tls_socket_write(CONNECTION *conn, const char *buf, size_t len)
{
  tlssockdata *data = conn->sockdata;
  int ret;
  size_t sent = 0;

  if (!data)
  {
    mutt_error(_("Error: no TLS socket open"));
    mutt_sleep(2);
    return -1;
  }

  do
  {
    do
    {
      ret = gnutls_record_send(data->state, buf + sent, len - sent);
    } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);

    if (ret < 0)
    {
      mutt_error("tls_socket_write (%s)", gnutls_strerror(ret));
      mutt_sleep(4);
      return -1;
    }

    sent += ret;
  } while (sent < len);

  return sent;
}

static int tls_socket_poll(CONNECTION *conn, time_t wait_secs)
{
  tlssockdata *data = conn->sockdata;

  if (!data)
    return -1;

  if (gnutls_record_check_pending(data->state))
    return 1;
  else
    return raw_socket_poll(conn, wait_secs);
}

static int tls_socket_open(CONNECTION *conn)
{
  if (raw_socket_open(conn) < 0)
    return -1;

  if (tls_negotiate(conn) < 0)
  {
    tls_socket_close(conn);
    return -1;
  }

  return 0;
}

int mutt_ssl_starttls(CONNECTION *conn)
{
  if (mutt_socket_has_buffered_input(conn))
  {
    /* L10N:
       The server is not supposed to send data immediately after
       confirming STARTTLS.  This warns the user that something
       weird is going on.
    */
    mutt_error _("Warning: clearing unexpected server data before TLS negotiation");
    mutt_sleep(0);
    mutt_socket_clear_buffered_input(conn);
  }

  if (tls_init() < 0)
    return -1;

  if (tls_negotiate(conn) < 0)
    return -1;

  conn->conn_read       = tls_socket_read;
  conn->conn_write      = tls_socket_write;
  conn->conn_close      = tls_starttls_close;
  conn->conn_poll       = tls_socket_poll;

  return 0;
}

/* Note: this function grabs the CN out of the client
 * cert but appears to do nothing with it.
 *
 * It does contain a call to mutt_account_getuser(), but this
 * interferes with SMTP client-cert authentication that doesn't use
 * AUTH EXTERNAL. (see gitlab #336)
 *
 * The mutt_sasl.c code sets up callbacks to get the login or user,
 * and it looks like the Cyrus SASL external code calls those.
 *
 * Brendan doesn't recall if this really was necessary at one time, so
 * I'm disabling it.
 */
#if 0
static void tls_get_client_cert(CONNECTION *conn)
{
  tlssockdata *data = conn->sockdata;
  const gnutls_datum_t *crtdata;
  gnutls_x509_crt_t clientcrt;
  char *cn = NULL;
  size_t cnlen = 0;
  int rc;

  /* get our cert CN if we have one */
  if (!(crtdata = gnutls_certificate_get_ours(data->state)))
    return;

  if (gnutls_x509_crt_init(&clientcrt) < 0)
  {
    muttdbg(1, "Failed to init gnutls crt");
    return;
  }
  if (gnutls_x509_crt_import(clientcrt, crtdata, GNUTLS_X509_FMT_DER) < 0)
  {
    muttdbg(1, "Failed to import gnutls client crt");
    goto err;
  }

  /* get length of CN, then grab it. */
  rc = gnutls_x509_crt_get_dn_by_oid(clientcrt, GNUTLS_OID_X520_COMMON_NAME,
                                     0, 0, NULL, &cnlen);
  if (((rc >= 0) || (rc == GNUTLS_E_SHORT_MEMORY_BUFFER)) &&
      cnlen > 0)
  {
    cn = safe_calloc(1, cnlen);
    if (gnutls_x509_crt_get_dn_by_oid(clientcrt, GNUTLS_OID_X520_COMMON_NAME,
                                      0, 0, cn, &cnlen) < 0)
      goto err;
    muttdbg(2, "client certificate CN: %s", cn);

    /* if we are using a client cert, SASL may expect an external auth name */
    mutt_account_getuser(&conn->account);
  }

err:
  FREE(&cn);
  gnutls_x509_crt_deinit(clientcrt);
}
#endif

#if HAVE_GNUTLS_PRIORITY_SET_DIRECT
static int tls_set_priority(tlssockdata *data)
{
  size_t nproto = 5;
  BUFFER *priority = NULL;
  int err, rv = -1;

  priority = mutt_buffer_pool_get();

  if (SslCiphers)
    mutt_buffer_strcpy(priority, SslCiphers);
  else
    mutt_buffer_strcpy(priority, "NORMAL");

  if (!option(OPTTLSV1_3))
  {
    nproto--;
    mutt_buffer_addstr(priority, ":-VERS-TLS1.3");
  }
  if (!option(OPTTLSV1_2))
  {
    nproto--;
    mutt_buffer_addstr(priority, ":-VERS-TLS1.2");
  }
  if (!option(OPTTLSV1_1))
  {
    nproto--;
    mutt_buffer_addstr(priority, ":-VERS-TLS1.1");
  }
  if (!option(OPTTLSV1))
  {
    nproto--;
    mutt_buffer_addstr(priority, ":-VERS-TLS1.0");
  }
  if (!option(OPTSSLV3))
  {
    nproto--;
    mutt_buffer_addstr(priority, ":-VERS-SSL3.0");
  }

  if (nproto == 0)
  {
    mutt_error(_("All available protocols for TLS/SSL connection disabled"));
    goto cleanup;
  }

  if ((err = gnutls_priority_set_direct(data->state, mutt_b2s(priority), NULL)) < 0)
  {
    mutt_error("gnutls_priority_set_direct(%s): %s", mutt_b2s(priority), gnutls_strerror(err));
    mutt_sleep(2);
    goto cleanup;
  }

  rv = 0;

cleanup:
  mutt_buffer_pool_release(&priority);
  return rv;
}
#else
/* This array needs to be large enough to hold all the possible values support
 * by Mutt.  The initialized values are just placeholders--the array gets
 * overwrriten in tls_negotiate() depending on the $ssl_use_* options.
 *
 * Note: gnutls_protocol_set_priority() was removed in GnuTLS version
 * 3.4 (2015-04).  TLS 1.3 support wasn't added until version 3.6.5.
 * Therefore, no attempt is made to support $ssl_use_tlsv1_3 in this code.
 */
static int protocol_priority[] = {GNUTLS_TLS1_2, GNUTLS_TLS1_1, GNUTLS_TLS1, GNUTLS_SSL3, 0};

static int tls_set_priority(tlssockdata *data)
{
  size_t nproto = 0; /* number of tls/ssl protocols */

  if (option(OPTTLSV1_2))
    protocol_priority[nproto++] = GNUTLS_TLS1_2;
  if (option(OPTTLSV1_1))
    protocol_priority[nproto++] = GNUTLS_TLS1_1;
  if (option(OPTTLSV1))
    protocol_priority[nproto++] = GNUTLS_TLS1;
  if (option(OPTSSLV3))
    protocol_priority[nproto++] = GNUTLS_SSL3;
  protocol_priority[nproto] = 0;

  if (nproto == 0)
  {
    mutt_error(_("All available protocols for TLS/SSL connection disabled"));
    return -1;
  }

  if (SslCiphers)
  {
    mutt_error(_("Explicit ciphersuite selection via $ssl_ciphers not supported"));
    mutt_sleep(2);
  }

  /* We use default priorities (see gnutls documentation),
     except for protocol version */
  gnutls_set_default_priority(data->state);
  gnutls_protocol_set_priority(data->state, protocol_priority);
  return 0;
}
#endif

/* tls_negotiate: After TLS state has been initialized, attempt to negotiate
 *   TLS over the wire, including certificate checks. */
static int tls_negotiate(CONNECTION * conn)
{
  tlssockdata *data;
  int err;
  char *hostname;

  data = (tlssockdata *) safe_calloc(1, sizeof(tlssockdata));
  conn->sockdata = data;
  err = gnutls_certificate_allocate_credentials(&data->xcred);
  if (err < 0)
  {
    FREE(&conn->sockdata);
    mutt_error("gnutls_certificate_allocate_credentials: %s", gnutls_strerror(err));
    mutt_sleep(2);
    return -1;
  }
  gnutls_certificate_set_pin_function(data->xcred, tls_passwd_cb,
                                      &conn->account);

  gnutls_certificate_set_x509_trust_file(data->xcred, SslCertFile,
                                         GNUTLS_X509_FMT_PEM);
  /* ignore errors, maybe file doesn't exist yet */

  if (SslCACertFile)
  {
    gnutls_certificate_set_x509_trust_file(data->xcred, SslCACertFile,
                                           GNUTLS_X509_FMT_PEM);
  }

  if (SslClientCert)
  {
    muttdbg(2, "Using client certificate %s", SslClientCert);
    gnutls_certificate_set_x509_key_file(data->xcred, SslClientCert,
                                         SslClientCert, GNUTLS_X509_FMT_PEM);
  }

#if HAVE_DECL_GNUTLS_VERIFY_DISABLE_TIME_CHECKS
  /* disable checking certificate activation/expiration times
     in gnutls, we do the checks ourselves */
  gnutls_certificate_set_verify_flags(data->xcred, GNUTLS_VERIFY_DISABLE_TIME_CHECKS);
#endif

  if ((err = gnutls_init(&data->state, GNUTLS_CLIENT)))
  {
    mutt_error("gnutls_init(): %s", gnutls_strerror(err));
    mutt_sleep(2);
    goto fail;
  }

  /* set socket */
  gnutls_transport_set_ptr(data->state, (gnutls_transport_ptr_t)(long)conn->fd);

  hostname = SslVerifyHostOverride ? SslVerifyHostOverride : conn->account.host;
  if (gnutls_server_name_set(data->state, GNUTLS_NAME_DNS, hostname,
                             mutt_strlen(hostname)))
  {
    mutt_error _("Warning: unable to set TLS SNI host name");
    mutt_sleep(1);
  }

  if (tls_set_priority(data) < 0)
  {
    goto fail;
  }

  if (SslDHPrimeBits > 0)
  {
    gnutls_dh_set_prime_bits(data->state, SslDHPrimeBits);
  }

  gnutls_credentials_set(data->state, GNUTLS_CRD_CERTIFICATE, data->xcred);

  do
  {
    err = gnutls_handshake(data->state);
  } while (err == GNUTLS_E_AGAIN || err == GNUTLS_E_INTERRUPTED);

  if (err < 0)
  {
    if (err == GNUTLS_E_FATAL_ALERT_RECEIVED)
    {
      mutt_error("gnutls_handshake: %s(%s)", gnutls_strerror(err),
                 gnutls_alert_get_name(gnutls_alert_get(data->state)));
    }
    else
    {
      mutt_error("gnutls_handshake: %s", gnutls_strerror(err));
    }
    mutt_sleep(2);
    goto fail;
  }

  if (!tls_check_certificate(conn))
    goto fail;

  /* set Security Strength Factor (SSF) for SASL */
  /* NB: gnutls_cipher_get_key_size() returns key length in bytes */
  conn->ssf = gnutls_cipher_get_key_size(gnutls_cipher_get(data->state)) * 8;

#if 0
  /* See comment above the tls_get_client_cert() function for why this
   * is ifdef'ed out.  Also note the SslClientCert is already set up
   * above. */
  tls_get_client_cert(conn);
#endif

  if (!option(OPTNOCURSES))
  {
    mutt_message(_("SSL/TLS connection using %s (%s/%s/%s)"),
                 gnutls_protocol_get_name(gnutls_protocol_get_version(data->state)),
                 gnutls_kx_get_name(gnutls_kx_get(data->state)),
                 gnutls_cipher_get_name(gnutls_cipher_get(data->state)),
                 gnutls_mac_get_name(gnutls_mac_get(data->state)));
    mutt_sleep(0);
  }

  return 0;

fail:
  gnutls_certificate_free_credentials(data->xcred);
  gnutls_deinit(data->state);
  FREE(&conn->sockdata);
  return -1;
}

static int tls_socket_close(CONNECTION *conn)
{
  tlssockdata *data = conn->sockdata;
  if (data)
  {
    /* shut down only the write half to avoid hanging waiting for the remote to respond.
     *
     * RFC5246 7.2.1. "Closure Alerts"
     *
     * It is not required for the initiator of the close to wait for the
     * responding close_notify alert before closing the read side of the
     * connection.
     */
    gnutls_bye(data->state, GNUTLS_SHUT_WR);

    gnutls_certificate_free_credentials(data->xcred);
    gnutls_deinit(data->state);
    FREE(&conn->sockdata);
  }

  return raw_socket_close(conn);
}

static int tls_starttls_close(CONNECTION *conn)
{
  int rc;

  rc = tls_socket_close(conn);
  conn->conn_read = raw_socket_read;
  conn->conn_write = raw_socket_write;
  conn->conn_close = raw_socket_close;
  conn->conn_poll = raw_socket_poll;

  return rc;
}

#define CERT_SEP "-----BEGIN"

/* this bit is based on read_ca_file() in gnutls */
static int tls_compare_certificates(const gnutls_datum_t *peercert)
{
  gnutls_datum_t cert;
  unsigned char *ptr;
  FILE *fd1;
  int ret;
  gnutls_datum_t b64_data;
  unsigned char *b64_data_data;
  struct stat filestat;

  if (stat(SslCertFile, &filestat) == -1)
    return 0;

  b64_data.size = filestat.st_size+1;
  b64_data_data = (unsigned char *) safe_calloc(1, b64_data.size);
  b64_data_data[b64_data.size-1] = '\0';
  b64_data.data = b64_data_data;

  fd1 = fopen(SslCertFile, "r");
  if (fd1 == NULL)
  {
    return 0;
  }

  b64_data.size = fread(b64_data.data, 1, b64_data.size, fd1);
  safe_fclose(&fd1);

  do
  {
    ret = gnutls_pem_base64_decode_alloc(NULL, &b64_data, &cert);
    if (ret != 0)
    {
      FREE(&b64_data_data);
      return 0;
    }

    /* find start of cert, skipping junk */
    ptr = (unsigned char *)strstr((char*)b64_data.data, CERT_SEP);
    if (!ptr)
    {
      gnutls_free(cert.data);
      FREE(&b64_data_data);
      return 0;
    }
    /* find start of next cert */
    ptr = (unsigned char *)strstr((char*)ptr + 1, CERT_SEP);

    b64_data.size = b64_data.size - (ptr - b64_data.data);
    b64_data.data = ptr;

    if (cert.size == peercert->size)
    {
      if (memcmp(cert.data, peercert->data, cert.size) == 0)
      {
        /* match found */
        gnutls_free(cert.data);
        FREE(&b64_data_data);
        return 1;
      }
    }

    gnutls_free(cert.data);
  } while (ptr != NULL);

  /* no match found */
  FREE(&b64_data_data);
  return 0;
}

static void tls_fingerprint(gnutls_digest_algorithm_t algo,
                            char *s, int l, const gnutls_datum_t *data)
{
  unsigned char md[64];
  size_t n;
  int j;

  n = 64;

  if (gnutls_fingerprint(algo, data, (char *)md, &n) < 0)
  {
    snprintf(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 ? " " : ""));
      safe_strcat(s, l, ch);
    }
    s[2*n+n/2-1] = '\0'; /* don't want trailing space */
  }
}

static char *tls_make_date(time_t t, char *s, size_t len)
{
  struct tm *l = gmtime(&t);

  if (l)
    snprintf(s, len,  "%s, %d %s %d %02d:%02d:%02d UTC",
             Weekdays[l->tm_wday], l->tm_mday, Months[l->tm_mon],
             l->tm_year + 1900, l->tm_hour, l->tm_min, l->tm_sec);
  else
    strfcpy(s, _("[invalid date]"), len);

  return (s);
}

static int tls_check_stored_hostname(const gnutls_datum_t *cert,
                                     const char *hostname)
{
  char buf[80];
  FILE *fp;
  char *linestr = NULL;
  size_t linestrsize;
  int linenum = 0;
  regex_t preg;
  regmatch_t pmatch[3];

  /* try checking against names stored in stored certs file */
  if ((fp = fopen(SslCertFile, "r")))
  {
    if (REGCOMP(&preg, "^#H ([a-zA-Z0-9_\\.-]+) ([0-9A-F]{4}( [0-9A-F]{4}){7})[ \t]*$",
                REG_ICASE) != 0)
    {
      safe_fclose(&fp);
      return 0;
    }

    buf[0] = '\0';
    tls_fingerprint(GNUTLS_DIG_MD5, buf, sizeof(buf), cert);
    while ((linestr = mutt_read_line(linestr, &linestrsize, fp, &linenum, 0)) != NULL)
    {
      if (linestr[0] == '#' && linestr[1] == 'H')
      {
        if (regexec(&preg, linestr, 3, pmatch, 0) == 0)
        {
          linestr[pmatch[1].rm_eo] = '\0';
          linestr[pmatch[2].rm_eo] = '\0';
          if (strcmp(linestr + pmatch[1].rm_so, hostname) == 0 &&
              strcmp(linestr + pmatch[2].rm_so, buf) == 0)
          {
            regfree(&preg);
            FREE(&linestr);
            safe_fclose(&fp);
            return 1;
          }
        }
      }
    }

    regfree(&preg);
    safe_fclose(&fp);
  }

  /* not found a matching name */
  return 0;
}

/* Returns 0 on success
 *        -1 on failure
 */
static int tls_check_preauth(const gnutls_datum_t *certdata,
                             gnutls_certificate_status_t certstat,
                             const char *hostname, int chainidx, int *certerr,
                             int *savedcert)
{
  gnutls_x509_crt_t cert;

  *certerr = CERTERR_VALID;
  *savedcert = 0;

  if (gnutls_x509_crt_init(&cert) < 0)
  {
    mutt_error(_("Error initialising gnutls certificate data"));
    mutt_sleep(2);
    return -1;
  }

  if (gnutls_x509_crt_import(cert, certdata, GNUTLS_X509_FMT_DER) < 0)
  {
    mutt_error(_("Error processing certificate data"));
    mutt_sleep(2);
    gnutls_x509_crt_deinit(cert);
    return -1;
  }

  /* Note: tls_negotiate() contains a call to
   * gnutls_certificate_set_verify_flags() with a flag disabling
   * GnuTLS checking of the dates.  So certstat shouldn't have the
   * GNUTLS_CERT_EXPIRED and GNUTLS_CERT_NOT_ACTIVATED bits set. */
  if (option(OPTSSLVERIFYDATES) != MUTT_NO)
  {
    if (gnutls_x509_crt_get_expiration_time(cert) < time(NULL))
      *certerr |= CERTERR_EXPIRED;
    if (gnutls_x509_crt_get_activation_time(cert) > time(NULL))
      *certerr |= CERTERR_NOTYETVALID;
  }

  if (chainidx == 0 && option(OPTSSLVERIFYHOST) != MUTT_NO
      && !gnutls_x509_crt_check_hostname(cert, hostname)
      && !tls_check_stored_hostname(certdata, hostname))
    *certerr |= CERTERR_HOSTNAME;

  if (certstat & GNUTLS_CERT_REVOKED)
  {
    *certerr |= CERTERR_REVOKED;
    certstat ^= GNUTLS_CERT_REVOKED;
  }

  /* see whether certificate is in our cache (certificates file) */
  if (tls_compare_certificates(certdata))
  {
    *savedcert = 1;

    /* We check above for certs with bad dates or that are revoked.
     * These must be accepted manually each time.  Otherwise, we
     * accept saved certificates as valid. */
    if (*certerr == CERTERR_VALID)
    {
      gnutls_x509_crt_deinit(cert);
      return 0;
    }
  }

  if (certstat & GNUTLS_CERT_INVALID)
  {
    *certerr |= CERTERR_NOTTRUSTED;
    certstat ^= GNUTLS_CERT_INVALID;
  }

  if (certstat & GNUTLS_CERT_SIGNER_NOT_FOUND)
  {
    /* NB: already cleared if cert in cache */
    *certerr |= CERTERR_NOTTRUSTED;
    certstat ^= GNUTLS_CERT_SIGNER_NOT_FOUND;
  }

  if (certstat & GNUTLS_CERT_SIGNER_NOT_CA)
  {
    /* NB: already cleared if cert in cache */
    *certerr |= CERTERR_SIGNERNOTCA;
    certstat ^= GNUTLS_CERT_SIGNER_NOT_CA;
  }

  if (certstat & GNUTLS_CERT_INSECURE_ALGORITHM)
  {
    /* NB: already cleared if cert in cache */
    *certerr |= CERTERR_INSECUREALG;
    certstat ^= GNUTLS_CERT_INSECURE_ALGORITHM;
  }

  /* we've been zeroing the interesting bits in certstat -
   * don't return OK if there are any unhandled bits we don't
   * understand */
  if (certstat != 0)
    *certerr |= CERTERR_OTHER;

  gnutls_x509_crt_deinit(cert);

  if (*certerr == CERTERR_VALID)
    return 0;

  return -1;
}

/* Returns 1 on success.
 *         0 on failure.
 */
static int tls_check_one_certificate(const gnutls_datum_t *certdata,
                                     gnutls_certificate_status_t certstat,
                                     const char *hostname, int idx, int len)
{
  int certerr, savedcert;
  gnutls_x509_crt_t cert;
  char buf[SHORT_STRING];
  char fpbuf[SHORT_STRING];
  size_t buflen;
  char dn_common_name[SHORT_STRING];
  char dn_email[SHORT_STRING];
  char dn_organization[SHORT_STRING];
  char dn_organizational_unit[SHORT_STRING];
  char dn_locality[SHORT_STRING];
  char dn_province[SHORT_STRING];
  char dn_country[SHORT_STRING];
  time_t t;
  char datestr[30];
  MUTTMENU *menu;
  char helpstr[LONG_STRING];
  char title[STRING];
  BUFFER *drow = NULL;
  FILE *fp;
  gnutls_datum_t pemdata;
  int done, ret, reset_ignoremacro = 0;

  if (!tls_check_preauth(certdata, certstat, hostname, idx, &certerr,
                         &savedcert))
    return 1;

  if (option(OPTNOCURSES))
  {
    muttdbg(1, "unable to prompt for certificate in batch mode");
    mutt_error _("Untrusted server certificate");
    return 0;
  }

  /* interactive check from user */
  if (gnutls_x509_crt_init(&cert) < 0)
  {
    mutt_error(_("Error initialising gnutls certificate data"));
    mutt_sleep(2);
    return 0;
  }

  if (gnutls_x509_crt_import(cert, certdata, GNUTLS_X509_FMT_DER) < 0)
  {
    mutt_error(_("Error processing certificate data"));
    mutt_sleep(2);
    gnutls_x509_crt_deinit(cert);
    return 0;
  }

  drow = mutt_buffer_pool_get();

  menu = mutt_new_menu(MENU_GENERIC);
  mutt_push_current_menu(menu);


  buflen = sizeof(dn_common_name);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0,
                                    dn_common_name, &buflen) != 0)
    dn_common_name[0] = '\0';
  buflen = sizeof(dn_email);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_PKCS9_EMAIL, 0, 0,
                                    dn_email, &buflen) != 0)
    dn_email[0] = '\0';
  buflen = sizeof(dn_organization);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_ORGANIZATION_NAME, 0, 0,
                                    dn_organization, &buflen) != 0)
    dn_organization[0] = '\0';
  buflen = sizeof(dn_organizational_unit);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_ORGANIZATIONAL_UNIT_NAME, 0, 0,
                                    dn_organizational_unit, &buflen) != 0)
    dn_organizational_unit[0] = '\0';
  buflen = sizeof(dn_locality);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_LOCALITY_NAME, 0, 0,
                                    dn_locality, &buflen) != 0)
    dn_locality[0] = '\0';
  buflen = sizeof(dn_province);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_STATE_OR_PROVINCE_NAME, 0, 0,
                                    dn_province, &buflen) != 0)
    dn_province[0] = '\0';
  buflen = sizeof(dn_country);
  if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COUNTRY_NAME, 0, 0,
                                    dn_country, &buflen) != 0)
    dn_country[0] = '\0';

  mutt_menu_add_dialog_row(menu, _("This certificate belongs to:"));
  mutt_buffer_printf(drow, "   %s  %s", dn_common_name, dn_email);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "   %s", dn_organization);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "   %s", dn_organizational_unit);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "   %s  %s  %s",
                     dn_locality, dn_province, dn_country);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));


  buflen = sizeof(dn_common_name);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0,
                                           dn_common_name, &buflen) != 0)
    dn_common_name[0] = '\0';
  buflen = sizeof(dn_email);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_PKCS9_EMAIL, 0, 0,
                                           dn_email, &buflen) != 0)
    dn_email[0] = '\0';
  buflen = sizeof(dn_organization);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_X520_ORGANIZATION_NAME, 0, 0,
                                           dn_organization, &buflen) != 0)
    dn_organization[0] = '\0';
  buflen = sizeof(dn_organizational_unit);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_X520_ORGANIZATIONAL_UNIT_NAME, 0, 0,
                                           dn_organizational_unit, &buflen) != 0)
    dn_organizational_unit[0] = '\0';
  buflen = sizeof(dn_locality);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_X520_LOCALITY_NAME, 0, 0,
                                           dn_locality, &buflen) != 0)
    dn_locality[0] = '\0';
  buflen = sizeof(dn_province);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_X520_STATE_OR_PROVINCE_NAME, 0, 0,
                                           dn_province, &buflen) != 0)
    dn_province[0] = '\0';
  buflen = sizeof(dn_country);
  if (gnutls_x509_crt_get_issuer_dn_by_oid(cert, GNUTLS_OID_X520_COUNTRY_NAME, 0, 0,
                                           dn_country, &buflen) != 0)
    dn_country[0] = '\0';

  mutt_menu_add_dialog_row(menu, "");
  mutt_menu_add_dialog_row(menu, _("This certificate was issued by:"));
  mutt_buffer_printf(drow, "   %s  %s", dn_common_name, dn_email);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "   %s", dn_organization);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "   %s", dn_organizational_unit);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "   %s  %s  %s",
                     dn_locality, dn_province, dn_country);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));


  mutt_menu_add_dialog_row(menu, "");
  mutt_menu_add_dialog_row(menu, _("This certificate is valid"));

  t = gnutls_x509_crt_get_activation_time(cert);
  mutt_buffer_printf(drow, _("   from %s"),
                     tls_make_date(t, datestr, 30));
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));

  t = gnutls_x509_crt_get_expiration_time(cert);
  mutt_buffer_printf(drow, _("     to %s"),
                     tls_make_date(t, datestr, 30));
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));


  fpbuf[0] = '\0';
  tls_fingerprint(GNUTLS_DIG_SHA, fpbuf, sizeof(fpbuf), certdata);
  mutt_buffer_printf(drow, _("SHA1 Fingerprint: %s"), fpbuf);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  fpbuf[0] = '\0';
  fpbuf[40] = '\0';  /* Ensure the second printed line is null terminated */
  tls_fingerprint(GNUTLS_DIG_SHA256, fpbuf, sizeof(fpbuf), certdata);
  fpbuf[39] = '\0';  /* Divide into two lines of output */
  mutt_buffer_printf(drow, "%s%s", _("SHA256 Fingerprint: "), fpbuf);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));
  mutt_buffer_printf(drow, "%*s%s",
                     (int)mutt_strlen(_("SHA256 Fingerprint: ")), "", fpbuf + 40);
  mutt_menu_add_dialog_row(menu, mutt_b2s(drow));


  if (certerr)
    mutt_menu_add_dialog_row(menu, "");
  if (certerr & CERTERR_NOTYETVALID)
    mutt_menu_add_dialog_row(menu, _("WARNING: Server certificate is not yet valid"));
  if (certerr & CERTERR_EXPIRED)
    mutt_menu_add_dialog_row(menu, _("WARNING: Server certificate has expired"));
  if (certerr & CERTERR_REVOKED)
    mutt_menu_add_dialog_row(menu, _("WARNING: Server certificate has been revoked"));
  if (certerr & CERTERR_HOSTNAME)
    mutt_menu_add_dialog_row(menu, _("WARNING: Server hostname does not match certificate"));
  if (certerr & CERTERR_SIGNERNOTCA)
    mutt_menu_add_dialog_row(menu, _("WARNING: Signer of server certificate is not a CA"));
  if (certerr & CERTERR_INSECUREALG)
    mutt_menu_add_dialog_row(menu,
                             _("Warning: Server certificate was signed using an insecure algorithm"));

  snprintf(title, sizeof(title),
           _("SSL Certificate check (certificate %d of %d in chain)"),
           len - idx, len);
  menu->title = title;
  /* certificates with bad dates, or that are revoked, must be
     accepted manually each and every time */
  if (SslCertFile && !savedcert
      && !(certerr & (CERTERR_EXPIRED | CERTERR_NOTYETVALID
                      | CERTERR_REVOKED)))
  {
    menu->prompt = _("(r)eject, accept (o)nce, (a)ccept always");
    /* L10N:
     * These three letters correspond to the choices in the string:
     * (r)eject, accept (o)nce, (a)ccept always.
     * This is an interactive certificate confirmation prompt for
     * a GNUTLS connection.
     */
    menu->keys = _("roa");
  }
  else
  {
    menu->prompt = _("(r)eject, accept (o)nce");
    /* L10N:
     * These two letters correspond to the choices in the string:
     * (r)eject, accept (o)nce.
     * These is an interactive certificate confirmation prompt for
     * a GNUTLS connection.
     */
    menu->keys = _("ro");
  }

  helpstr[0] = '\0';
  mutt_make_help(buf, sizeof(buf), _("Exit  "), MENU_GENERIC, OP_EXIT);
  safe_strcat(helpstr, sizeof(helpstr), buf);
  mutt_make_help(buf, sizeof(buf), _("Help"), MENU_GENERIC, OP_HELP);
  safe_strcat(helpstr, sizeof(helpstr), buf);
  menu->help = helpstr;

  done = 0;
  if (!option(OPTIGNOREMACROEVENTS))
  {
    set_option(OPTIGNOREMACROEVENTS);
    reset_ignoremacro = 1;
  }
  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")))
        {
          /* save hostname if necessary */
          if (certerr & CERTERR_HOSTNAME)
          {
            fpbuf[0] = '\0';
            tls_fingerprint(GNUTLS_DIG_MD5, fpbuf, sizeof(fpbuf), certdata);
            fprintf(fp, "#H %s %s\n", hostname, fpbuf);
            done = 1;
          }
          /* Save the cert for all other errors */
          if (certerr ^ CERTERR_HOSTNAME)
          {
            done = 0;
            ret = gnutls_pem_base64_encode_alloc("CERTIFICATE", certdata,
                                                 &pemdata);
            if (ret == 0)
            {
              if (fwrite(pemdata.data, pemdata.size, 1, fp) == 1)
              {
                done = 1;
              }
              gnutls_free(pemdata.data);
            }
          }
          safe_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;
        break;
    }
  }
  if (reset_ignoremacro)
    unset_option(OPTIGNOREMACROEVENTS);

  mutt_buffer_pool_release(&drow);
  mutt_pop_current_menu(menu);
  mutt_menuDestroy(&menu);
  gnutls_x509_crt_deinit(cert);

  return (done == 2) ? 1 : 0;
}

/* sanity-checking wrapper for gnutls_certificate_verify_peers.
 *
 * certstat is technically a bitwise-or of gnutls_certificate_status_t
 * values.
 *
 * Returns:
 *   - 0 if certstat was set. note: this does not mean success.
 *   - nonzero on failure.
 */
static int tls_verify_peers(gnutls_session_t tlsstate,
                            gnutls_certificate_status_t *certstat)
{
  int verify_ret;

  /* gnutls_certificate_verify_peers2() chains to
   * gnutls_x509_trust_list_verify_crt2().  That function's documentation says:
   *
   *   When a certificate chain of cert_list_size with more than one
   *   certificates is provided, the verification status will apply to
   *   the first certificate in the chain that failed
   *   verification. The verification process starts from the end of
   *   the chain (from CA to end certificate). The first certificate
   *   in the chain must be the end-certificate while the rest of the
   *   members may be sorted or not.
   *
   * This is why tls_check_certificate() loops from CA to host in that order,
   * calling the menu, and recalling tls_verify_peers() for each approved
   * cert in the chain.
   */
  verify_ret = gnutls_certificate_verify_peers2(tlsstate, certstat);

  /* certstat was set */
  if (!verify_ret)
    return 0;

  if (verify_ret == GNUTLS_E_NO_CERTIFICATE_FOUND)
    mutt_error(_("Unable to get certificate from peer"));
  else
    mutt_error(_("Certificate verification error (%s)"),
               gnutls_strerror(verify_ret));

  mutt_sleep(2);
  return verify_ret;
}

/* Returns 1 on success.
 *         0 on failure.
 */
static int tls_check_certificate(CONNECTION *conn)
{
  tlssockdata *data = conn->sockdata;
  gnutls_session_t state = data->state;
  const gnutls_datum_t *cert_list;
  unsigned int cert_list_size = 0;
  gnutls_certificate_status_t certstat;
  int certerr, i, preauthrc, savedcert, rc = 0;
  int max_preauth_pass = -1;
  int rcsettrust;
  char *hostname;

  hostname = SslVerifyHostOverride ? SslVerifyHostOverride : conn->account.host;

  /* tls_verify_peers() calls gnutls_certificate_verify_peers2(),
   * which verifies the auth_type is GNUTLS_CRD_CERTIFICATE
   * and that get_certificate_type() for the server is GNUTLS_CRT_X509.
   * If it returns 0, certstat will be set with failure codes for the first
   * cert in the chain (from CA to host) with an error.
   */
  if (tls_verify_peers(state, &certstat) != 0)
    return 0;

  cert_list = gnutls_certificate_get_peers(state, &cert_list_size);
  if (!cert_list)
  {
    mutt_error(_("Unable to get certificate from peer"));
    mutt_sleep(2);
    return 0;
  }

  /* tls_verify_peers doesn't check hostname or expiration, so walk
   * from most specific to least checking these. If we see a saved certificate,
   * its status short-circuits the remaining checks. */
  preauthrc = 0;
  for (i = 0; i < cert_list_size; i++)
  {
    rc = tls_check_preauth(&cert_list[i], certstat, hostname, i,
                           &certerr, &savedcert);
    preauthrc += rc;
    if (!preauthrc)
      max_preauth_pass = i;

    if (savedcert)
    {
      if (!preauthrc)
        return 1;
      else
        break;
    }
  }

  /* then check interactively, starting from chain root */
  for (i = cert_list_size - 1; i >= 0; i--)
  {
    rc = tls_check_one_certificate(&cert_list[i], certstat, hostname,
                                   i, cert_list_size);

    /* Stop checking if the menu cert is aborted or rejected. */
    if (!rc)
      break;

    /* add signers to trust set, then reverify */
    if (i)
    {
      rcsettrust = gnutls_certificate_set_x509_trust_mem(data->xcred,
                                                         &cert_list[i],
                                                         GNUTLS_X509_FMT_DER);
      if (rcsettrust != 1)
        muttdbg(1, "error trusting certificate %d: %d", i, rcsettrust);

      if (tls_verify_peers(state, &certstat) != 0)
        return 0;

      /* If the cert chain now verifies, and all lower certs already
       * passed preauth, we are done.
       */
      if (!certstat && (max_preauth_pass >= i - 1))
        return 1;
    }
  }

  return rc;
}

static void client_cert_prompt(char *prompt, size_t prompt_size, ACCOUNT *account)
{
  /* L10N:
     When using a $ssl_client_cert, GNUTLS may prompt for the password
     to decrypt the cert.  %s is the hostname.
  */
  snprintf(prompt, prompt_size, _("Password for %s client cert: "),
           account->host);
}

static int tls_passwd_cb(void *userdata, int attempt, const char *token_url,
                         const char *token_label,
                         unsigned int flags,
                         char *buf, size_t size)
{
  ACCOUNT *account;

  if (!buf || size <= 0 || !userdata)
    return GNUTLS_E_INVALID_PASSWORD;

  account = (ACCOUNT *) userdata;

  if (_mutt_account_getpass(account, client_cert_prompt))
    return GNUTLS_E_INVALID_PASSWORD;

  snprintf(buf, size, "%s", account->pass);
  return GNUTLS_E_SUCCESS;
}
