/* * Copyright notice from original mutt: * Copyright (C) 2001 Marco d'Itri * Copyright (C) 2001-2004 Andrew McDonald * * 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. */ #include #include #include #include #include #include "mutt.h" #include "mutt_socket.h" @import "../lib-lua/base.cpkg" @package mod_ssl { bool force_tls = 0; /* ** .pp ** If this variable is \fIset\fP, Madmutt will require that all connections ** to remote servers be encrypted. Furthermore it will attempt to ** negotiate TLS even if the server does not advertise the capability, ** since it would otherwise have to abort the connection anyway. This ** option supersedes ``$$ssl_starttls''. */ bool starttls = 1; /* ** .pp ** If \fIset\fP (the default), Madmutt will attempt to use STARTTLS on servers ** advertising the capability. When \fIunset\fP, Madmutt will not attempt to ** use STARTTLS regardless of the server's capabilities. */ bool use_sslv3 = 1; /* ** .pp ** This variables specifies whether to attempt to use SSLv3 in the ** SSL authentication process. */ bool use_tlsv1 = 1; /* ** .pp ** This variables specifies whether to attempt to use TLSv1 in the ** SSL authentication process. */ int min_dh_prime_bits = 0; /* ** .pp ** This variable specifies the minimum acceptable prime size (in bits) ** for use in any Diffie-Hellman key exchange. A value of 0 will use ** the default from the GNUTLS library. */ path_t cert_file = luaM_pathnew("~/.cache/madmutt/certificates"); /* ** .pp ** This variable specifies the file where the certificates you trust ** are saved. When an unknown certificate is encountered, you are asked ** if you accept it or not. If you accept it, the certificate can also ** be saved in this file and further connections are automatically ** accepted. ** .pp ** You can also manually add CA certificates in this file. Any server ** certificate that is signed with one of these CA certificates are ** also automatically accepted. ** .pp ** Example: \fTset certificate_file=~/.madmutt/certificates\fP */ path_t ca_certificates_file = NULL; /* ** .pp ** This variable specifies a file containing trusted CA certificates. ** Any server certificate that is signed with one of these CA ** certificates are also automatically accepted. ** .pp ** Example: \fTset ssl_ca_certificates_file=/etc/ssl/certs/ca-certificates.crt\fP */ }; typedef struct _tlssockdata { gnutls_session state; gnutls_certificate_credentials xcred; } tlssockdata; /* local prototypes */ static int tls_socket_read (CONNECTION * conn, char *buf, ssize_t len); static int tls_socket_write (CONNECTION * conn, const char *buf, ssize_t len); 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_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; return 0; } static int tls_socket_read (CONNECTION * conn, char *buf, ssize_t len) { tlssockdata *data = conn->sockdata; int ret; if (!data) { mutt_error (_("Error: no TLS socket open")); mutt_sleep (2); return -1; } ret = gnutls_record_recv (data->state, buf, len); if (gnutls_error_is_fatal (ret) == 1) { 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, ssize_t len) { tlssockdata *data = conn->sockdata; int ret; if (!data) { mutt_error (_("Error: no TLS socket open")); mutt_sleep (2); return -1; } ret = gnutls_record_send (data->state, buf, len); if (gnutls_error_is_fatal (ret) == 1) { mutt_error (_("tls_socket_write (%s)"), gnutls_strerror (ret)); mutt_sleep (4); return -1; } return ret; } 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 (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; return 0; } static int protocol_priority[] = { GNUTLS_TLS1, GNUTLS_SSL3, 0 }; /* tls_negotiate: After TLS state has been initialised, attempt to negotiate * TLS over the wire, including certificate checks. */ static int tls_negotiate (CONNECTION * conn) { tlssockdata *data; int err; data = p_new(tlssockdata, 1); conn->sockdata = data; err = gnutls_certificate_allocate_credentials (&data->xcred); if (err < 0) { p_delete(&conn->sockdata); mutt_error (_("gnutls_certificate_allocate_credentials: %s"), gnutls_strerror (err)); mutt_sleep (2); return -1; } gnutls_certificate_set_x509_trust_file (data->xcred, mod_ssl.cert_file, GNUTLS_X509_FMT_PEM); /* ignore errors, maybe file doesn't exist yet */ if (mod_ssl.ca_certificates_file) { gnutls_certificate_set_x509_trust_file (data->xcred, mod_ssl.ca_certificates_file, GNUTLS_X509_FMT_PEM); } /* gnutls_set_x509_client_key (data->xcred, "", ""); gnutls_set_x509_cert_callback (data->xcred, cert_callback); */ gnutls_init (&data->state, GNUTLS_CLIENT); /* set socket */ gnutls_transport_set_ptr (data->state, (gnutls_transport_ptr)(intptr_t)conn->fd); /* disable TLS/SSL protocols as needed */ if (!mod_ssl.use_tlsv1 && !mod_ssl.use_sslv3) { mutt_error (_("All available protocols for TLS/SSL connection disabled")); goto fail; } else if (!mod_ssl.use_tlsv1) { protocol_priority[0] = GNUTLS_SSL3; protocol_priority[1] = 0; } else if (!mod_ssl.use_sslv3) { protocol_priority[0] = GNUTLS_TLS1; protocol_priority[1] = 0; } /* else use the list set above */ /* 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); if (mod_ssl.min_dh_prime_bits > 0) { gnutls_dh_set_prime_bits(data->state, mod_ssl.min_dh_prime_bits); } /* gnutls_set_cred (data->state, GNUTLS_ANON, NULL); */ gnutls_credentials_set (data->state, GNUTLS_CRD_CERTIFICATE, data->xcred); err = gnutls_handshake (data->state); while (err == GNUTLS_E_AGAIN || err == GNUTLS_E_INTERRUPTED) { err = gnutls_handshake (data->state); } 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; 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); p_delete(&conn->sockdata); return -1; } static int tls_socket_close (CONNECTION * conn) { tlssockdata *data = conn->sockdata; if (data) { gnutls_bye (data->state, GNUTLS_SHUT_RDWR); gnutls_certificate_free_credentials (data->xcred); gnutls_deinit (data->state); p_delete(&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; return rc; } #define CERT_SEP "-----BEGIN" /* this bit is based on read_ca_file() in gnutls */ static int tls_compare_certificates (const gnutls_datum * peercert) { gnutls_datum cert; unsigned char *ptr; FILE *fd1; int ret; gnutls_datum b64_data; unsigned char *b64_data_data; struct stat filestat; if (stat(mod_ssl.cert_file, &filestat) == -1) return 0; b64_data.size = filestat.st_size + 1; b64_data_data = p_new(unsigned char, b64_data.size); b64_data_data[b64_data.size - 1] = '\0'; b64_data.data = b64_data_data; fd1 = fopen(mod_ssl.cert_file, "r"); if (fd1 == NULL) { return 0; } b64_data.size = fread (b64_data.data, 1, b64_data.size, fd1); m_fclose(&fd1); do { ret = gnutls_pem_base64_decode_alloc (NULL, &b64_data, &cert); if (ret != 0) { p_delete(&b64_data_data); return 0; } ptr = (unsigned char *) strstr ((char*) b64_data.data, CERT_SEP) + 1; ptr = (unsigned char *) strstr ((char*) ptr, 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); p_delete(&b64_data_data); return 1; } } gnutls_free (cert.data); } while (ptr != NULL); /* no match found */ p_delete(&b64_data_data); return 0; } static void tls_fingerprint (gnutls_digest_algorithm algo, char *s, int l, const gnutls_datum * data) { char md[36]; ssize_t n; int j; n = 36; if (gnutls_fingerprint(algo, data, md, (size_t *)&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 ? " " : "")); strncat (s, ch, l); } s[2 * n + n / 2 - 1] = '\0'; /* don't want trailing space */ } } static char *tls_make_date (time_t t, char *s, ssize_t len) { struct tm *l = gmtime(&t); if (l) { const char *loc; loc = setlocale(LC_TIME, "C"); strftime(s, len, "%a, %d %b %Y %T UTC", l); setlocale(LC_TIME, loc); } else { m_strcpy(s, len, _("[invalid date]")); } return (s); } static int tls_check_stored_hostname (const gnutls_datum * cert, const char *hostname) { char buf[80]; FILE *fp; char *linestr = NULL; ssize_t linestrsize; int linenum = 0; regex_t preg; regmatch_t pmatch[3]; /* try checking against names stored in stored certs file */ if ((fp = fopen(mod_ssl.cert_file, "r"))) { if (regcomp (&preg, "^#H ([a-zA-Z0-9_\\.-]+) ([0-9A-F]{4}( [0-9A-F]{4}){7})[ \t]*$", REG_ICASE | REG_EXTENDED) != 0) { regfree (&preg); return 0; } buf[0] = '\0'; tls_fingerprint (GNUTLS_DIG_MD5, buf, sizeof (buf), cert); while ((linestr = mutt_read_line (linestr, &linestrsize, fp, &linenum)) != 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 (m_strcmp(linestr + pmatch[1].rm_so, hostname) == 0 && m_strcmp(linestr + pmatch[2].rm_so, buf) == 0) { regfree (&preg); p_delete(&linestr); m_fclose(&fp); return 1; } } } } regfree (&preg); m_fclose(&fp); } /* not found a matching name */ return 0; } static int tls_check_certificate (CONNECTION * conn) { tlssockdata *data = conn->sockdata; gnutls_session state = data->state; char fpbuf[STRING]; ssize_t buflen; char dn_common_name[STRING]; char dn_email[STRING]; char dn_organization[STRING]; char dn_organizational_unit[STRING]; char dn_locality[STRING]; char dn_province[STRING]; char dn_country[STRING]; MUTTMENU *menu; int done, row, i, ret; FILE *fp; time_t t; const gnutls_datum *cert_list; unsigned int cert_list_size = 0; gnutls_certificate_status_t certstat; char datestr[30]; gnutls_x509_crt cert; gnutls_datum pemdata; int certerr_expired = 0; int certerr_notyetvalid = 0; int certerr_hostname = 0; int certerr_nottrusted = 0; int certerr_revoked = 0; int certerr_signernotca = 0; if (gnutls_auth_get_type (state) != GNUTLS_CRD_CERTIFICATE) { mutt_error (_("Unable to get certificate from peer")); mutt_sleep (2); return 0; } if (gnutls_certificate_verify_peers2(state, &certstat) < 0) { mutt_error (_("Certificate verification error (%s)"), gnutls_strerror(certstat)); mutt_sleep (2); return 0; } /* We only support X.509 certificates (not OpenPGP) at the moment */ if (gnutls_certificate_type_get (state) != GNUTLS_CRT_X509) { mutt_error (_("Certificate is not X.509")); mutt_sleep (2); return 0; } if (gnutls_x509_crt_init (&cert) < 0) { mutt_error (_("Error initialising gnutls certificate data")); mutt_sleep (2); 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; } /* FIXME: Currently only check first certificate in chain. */ if (gnutls_x509_crt_import (cert, &cert_list[0], GNUTLS_X509_FMT_DER) < 0) { mutt_error (_("Error processing certificate data")); mutt_sleep (2); return 0; } if (gnutls_x509_crt_get_expiration_time (cert) < time (NULL)) { certerr_expired = 1; } if (gnutls_x509_crt_get_activation_time (cert) > time (NULL)) { certerr_notyetvalid = 1; } if (!gnutls_x509_crt_check_hostname (cert, conn->account.host) && !tls_check_stored_hostname (&cert_list[0], conn->account.host)) { certerr_hostname = 1; } /* see whether certificate is in our cache (certificates file) */ if (tls_compare_certificates (&cert_list[0])) { if (certstat & GNUTLS_CERT_INVALID) { /* doesn't matter - have decided is valid because server certificate is in our trusted cache */ certstat ^= GNUTLS_CERT_INVALID; } if (certstat & GNUTLS_CERT_SIGNER_NOT_FOUND) { /* doesn't matter that we haven't found the signer, since certificate is in our trusted cache */ certstat ^= GNUTLS_CERT_SIGNER_NOT_FOUND; } if (certstat & GNUTLS_CERT_SIGNER_NOT_CA) { /* Hmm. Not really sure how to handle this, but let's say that we don't care if the CA certificate hasn't got the correct X.509 basic constraints if server certificate is in our cache. */ certstat ^= GNUTLS_CERT_SIGNER_NOT_CA; } } if (certstat & GNUTLS_CERT_REVOKED) { certerr_revoked = 1; certstat ^= GNUTLS_CERT_REVOKED; } if (certstat & GNUTLS_CERT_INVALID) { certerr_nottrusted = 1; certstat ^= GNUTLS_CERT_INVALID; } if (certstat & GNUTLS_CERT_SIGNER_NOT_FOUND) { /* NB: already cleared if cert in cache */ certerr_nottrusted = 1; certstat ^= GNUTLS_CERT_SIGNER_NOT_FOUND; } if (certstat & GNUTLS_CERT_SIGNER_NOT_CA) { /* NB: already cleared if cert in cache */ certerr_signernotca = 1; certstat ^= GNUTLS_CERT_SIGNER_NOT_CA; } /* OK if signed by (or is) a trusted certificate */ /* we've been zeroing the interesting bits in certstat - don't return OK if there are any unhandled bits we don't understand */ if (!(certerr_expired || certerr_notyetvalid || certerr_hostname || certerr_nottrusted) && certstat == 0) { gnutls_x509_crt_deinit (cert); return 1; } /* interactive check from user */ menu = mutt_new_menu (); menu->max = 25; menu->dialog = p_new(char*, menu->max); for (i = 0; i < menu->max; i++) menu->dialog[i] = p_new(char, STRING); row = 0; m_strcpy(menu->dialog[row], STRING, _("This certificate belongs to:")); row++; buflen = sizeof (dn_common_name); if (gnutls_x509_crt_get_dn_by_oid (cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, dn_common_name, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&buflen) != 0) dn_country[0] = '\0'; snprintf (menu->dialog[row++], STRING, " %s %s", dn_common_name, dn_email); snprintf (menu->dialog[row++], STRING, " %s", dn_organization); snprintf (menu->dialog[row++], STRING, " %s", dn_organizational_unit); snprintf (menu->dialog[row++], STRING, " %s %s %s", dn_locality, dn_province, dn_country); row++; m_strcpy(menu->dialog[row], STRING, _("This certificate was issued by:")); row++; 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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&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, (size_t *)&buflen) != 0) dn_country[0] = '\0'; snprintf (menu->dialog[row++], STRING, " %s %s", dn_common_name, dn_email); snprintf (menu->dialog[row++], STRING, " %s", dn_organization); snprintf (menu->dialog[row++], STRING, " %s", dn_organizational_unit); snprintf (menu->dialog[row++], STRING, " %s %s %s", dn_locality, dn_province, dn_country); row++; snprintf (menu->dialog[row++], STRING, _("This certificate is valid")); t = gnutls_x509_crt_get_activation_time (cert); snprintf (menu->dialog[row++], STRING, _(" from %s"), tls_make_date (t, datestr, 30)); t = gnutls_x509_crt_get_expiration_time (cert); snprintf (menu->dialog[row++], STRING, _(" to %s"), tls_make_date (t, datestr, 30)); fpbuf[0] = '\0'; tls_fingerprint (GNUTLS_DIG_SHA, fpbuf, sizeof (fpbuf), &cert_list[0]); snprintf (menu->dialog[row++], STRING, _("SHA1 Fingerprint: %s"), fpbuf); fpbuf[0] = '\0'; tls_fingerprint (GNUTLS_DIG_MD5, fpbuf, sizeof (fpbuf), &cert_list[0]); snprintf (menu->dialog[row++], STRING, _("MD5 Fingerprint: %s"), fpbuf); if (certerr_notyetvalid) { row++; m_strcpy(menu->dialog[row], STRING, _("WARNING: Server certificate is not yet valid")); } if (certerr_expired) { row++; m_strcpy(menu->dialog[row], STRING, _("WARNING: Server certificate has expired")); } if (certerr_revoked) { row++; m_strcpy(menu->dialog[row], STRING, _("WARNING: Server certificate has been revoked")); } if (certerr_hostname) { row++; m_strcpy(menu->dialog[row], STRING, _("WARNING: Server hostname does not match certificate")); } if (certerr_signernotca) { row++; m_strcpy(menu->dialog[row], STRING, _("WARNING: Signer of server certificate is not a CA")); } menu->title = _("TLS/SSL Certificate check"); /* certificates with bad dates, or that are revoked, must be accepted manually each and every time */ if (mod_ssl.cert_file && !certerr_expired && !certerr_notyetvalid && !certerr_revoked) { menu->prompt = _("(r)eject, accept (o)nce, (a)ccept always"); menu->keys = _("roa"); } else { menu->prompt = _("(r)eject, accept (o)nce"); menu->keys = _("ro"); } 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(mod_ssl.cert_file, "a"))) { /* save hostname if necessary */ if (certerr_hostname) { fprintf (fp, "#H %s %s\n", conn->account.host, fpbuf); done = 1; } if (certerr_nottrusted) { done = 0; ret = gnutls_pem_base64_encode_alloc ("CERTIFICATE", &cert_list[0], &pemdata); if (ret == 0) { if (fwrite (pemdata.data, pemdata.size, 1, fp) == 1) { done = 1; } gnutls_free (pemdata.data); } } m_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; } } unset_option (OPTUNBUFFEREDINPUT); mutt_menuDestroy (&menu); gnutls_x509_crt_deinit (cert); return (done == 2); } /* vim:set ft=c: */