/*
 * File: tls_mbedtls.c
 *
 * Copyright (C) 2011 Benjamin Johnson <obeythepenguin@users.sourceforge.net>
 * (for the https code offered from dplus browser that formed the basis...)
 * Copyright 2016 corvid
 * Copyright (C) 2023-2024 Rodrigo Arias Mallo <rodarima@gmail.com>
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 */

/*
 * https://www.ssllabs.com/ssltest/viewMyClient.html
 * https://badssl.com
 *
 * Using TLS in Applications: https://datatracker.ietf.org/wg/uta/documents/
 * TLS: https://datatracker.ietf.org/wg/tls/documents/
 */

#include "config.h"
#include "../msg.h"

#ifndef ENABLE_TLS
# error "ENABLE_TLS not defined"
#endif

#include <assert.h>
#include <errno.h>

#include "../../dlib/dlib.h"
#include "../dialog.hh"
#include "../klist.h"
#include "iowatch.hh"
#include "tls.h"
#include "Url.h"

#include <mbedtls/platform.h>  /* WORKAROUND: mbed TLS 2.3.0 ssl.h needs it */
#include <mbedtls/ssl.h>
#include <mbedtls/ctr_drbg.h>  /* random number generator */
#include <mbedtls/entropy.h>
#include <mbedtls/error.h>
#include <mbedtls/oid.h>
#include <mbedtls/x509.h>
#include <mbedtls/version.h>
#if MBEDTLS_VERSION_NUMBER < 0x03000000
#include <mbedtls/net.h>    /* net_send, net_recv */
#else
#include <mbedtls/net_sockets.h>    /* net_send, net_recv */
#endif

#define CERT_STATUS_NONE 0
#define CERT_STATUS_RECEIVING 1
#define CERT_STATUS_CLEAN 2
#define CERT_STATUS_BAD 3
#define CERT_STATUS_USER_ACCEPTED 4

typedef struct {
   char *hostname;
   int port;
   int cert_status;
} Server_t;

typedef struct {
   char *name;
   Dlist *servers;
} CertAuth_t;

typedef struct {
   int fd;
   int connkey;
} FdMapEntry_t;

/*
 * Data type for TLS connection information
 */
typedef struct {
   int fd;
   DilloUrl *url;
   mbedtls_ssl_context *ssl;
   bool_t connecting;
} Conn_t;

/* List of active TLS connections */
static Klist_t *conn_list = NULL;

static bool_t ssl_enabled = TRUE;
static mbedtls_ssl_config ssl_conf;
static mbedtls_x509_crt cacerts;
static mbedtls_ctr_drbg_context ctr_drbg;
static mbedtls_entropy_context entropy;

static Dlist *servers;
static Dlist *cert_authorities;
static Dlist *fd_map;

static void Tls_handshake_cb(int fd, void *vconnkey);


#if MBEDTLS_VERSION_NUMBER >= 0x03060000
/* Moved to ssl_ciphersuites_internal.h in mbedtls 3.6.0 */
int mbedtls_ssl_ciphersuite_uses_psk(const mbedtls_ssl_ciphersuite_t *info);
#endif

/*
 * Compare by FD.
 */
static int Tls_fd_map_cmp(const void *v1, const void *v2)
{
   int fd = VOIDP2INT(v2);
   const FdMapEntry_t *e = v1;

   return (fd != e->fd);
}

static void Tls_fd_map_add_entry(int fd, int connkey)
{
   FdMapEntry_t *e = dNew0(FdMapEntry_t, 1);
   e->fd = fd;
   e->connkey = connkey;

   if (dList_find_custom(fd_map, INT2VOIDP(e->fd), Tls_fd_map_cmp)) {
      MSG_ERR("TLS FD ENTRY ALREADY FOUND FOR %d\n", e->fd);
      assert(0);
   }

   dList_append(fd_map, e);
//MSG("ADD ENTRY %d %s\n", e->fd, URL_STR(sd->url));
}

/*
 * Remove and free entry from fd_map.
 */
static void Tls_fd_map_remove_entry(int fd)
{
   void *data = dList_find_custom(fd_map, INT2VOIDP(fd), Tls_fd_map_cmp);

//MSG("REMOVE ENTRY %d\n", fd);
   if (data) {
      dList_remove_fast(fd_map, data);
      dFree(data);
   } else {
      MSG("TLS FD ENTRY NOT FOUND FOR %d\n", fd);
   }
}

/*
 * Return TLS connection information for a given file
 * descriptor, or NULL if no TLS connection was found.
 */
void *a_Tls_mbedtls_connection(int fd)
{
   Conn_t *conn;

   if (fd_map) {
      FdMapEntry_t *fme = dList_find_custom(fd_map, INT2VOIDP(fd),
                                            Tls_fd_map_cmp);

      if (fme && (conn = a_Klist_get_data(conn_list, fme->connkey)))
         return conn;
   }
   return NULL;
}

/*
 * Add a new TLS connection information node.
 */
static Conn_t *Tls_conn_new(int fd, const DilloUrl *url,
                            mbedtls_ssl_context *ssl)
{
   Conn_t *conn = dNew0(Conn_t, 1);
   conn->fd = fd;
   conn->url = a_Url_dup(url);
   conn->ssl = ssl;
   conn->connecting = TRUE;
   return conn;
}

static int Tls_make_conn_key(Conn_t *conn)
{
   int key = a_Klist_insert(&conn_list, conn);

   Tls_fd_map_add_entry(conn->fd, key);

   return key;
}

/*
 * Load certificates from a given filename.
 */
static void Tls_load_certificates_from_file(const char *const filename)
{
   int ret = mbedtls_x509_crt_parse_file(&cacerts, filename);

   if (ret < 0) {
      if (ret == MBEDTLS_ERR_PK_FILE_IO_ERROR) {
         /* can't read from file */
      } else {
         MSG("Failed to parse certificates from %s (returned -0x%04x)\n",
             filename, -ret);
      }
   }
}

/*
 * Load certificates from a given pathname.
 */
static void Tls_load_certificates_from_path(const char *const pathname)
{
   int ret = mbedtls_x509_crt_parse_path(&cacerts, pathname);

   if (ret < 0) {
      if (ret == MBEDTLS_ERR_X509_FILE_IO_ERROR) {
         /* can't read from path */
      } else {
         MSG("Failed to parse certificates from %s (returned -0x%04x)\n",
             pathname, -ret);
      }
   }
}

/*
 * Remove duplicate certificates.
 */
static void Tls_remove_duplicate_certificates()
{
   mbedtls_x509_crt *cp, *curr = &cacerts;

   while (curr) {
      cp = curr;
      while (cp->next) {
         if (curr->serial.len == cp->next->serial.len &&
             !memcmp(curr->serial.p, cp->next->serial.p, curr->serial.len) &&
             curr->subject_raw.len == cp->next->subject_raw.len &&
             !memcmp(curr->subject_raw.p, cp->next->subject_raw.p,
                     curr->subject_raw.len)) {
            mbedtls_x509_crt *duplicate = cp->next;

            cp->next = duplicate->next;

            /* clearing the next field prevents it from freeing the whole
             * chain of certificates
             */
            duplicate->next = NULL;
            mbedtls_x509_crt_free(duplicate);
            dFree(duplicate);
         } else {
            cp = cp->next;
         }
      }
      curr = curr->next;
   }
}

/*
 * Load trusted certificates.
 */
static void Tls_load_certificates()
{
   /* curl-7.37.1 says that the following bundle locations are used on "Debian
    * systems", "Redhat and Mandriva", "old(er) Redhat", "FreeBSD", and
    * "OpenBSD", respectively -- and that the /etc/ssl/certs/ path is needed on
    * "SUSE". No doubt it's all changed some over time, but this gives us
    * something to work with.
    */
   uint_t u;
   char *userpath;
   mbedtls_x509_crt *curr;

   static const char *const ca_files[] = {
      "/etc/ssl/certs/ca-certificates.crt",
      "/etc/pki/tls/certs/ca-bundle.crt",
      "/usr/share/ssl/certs/ca-bundle.crt",
      "/usr/local/share/certs/ca-root.crt",
      "/etc/ssl/cert.pem",
      CA_CERTS_FILE
   };

   static const char *const ca_paths[] = {
      "/etc/ssl/certs/",
      CA_CERTS_DIR
   };

   for (u = 0; u < sizeof(ca_files)/sizeof(ca_files[0]); u++) {
      if (*ca_files[u])
         Tls_load_certificates_from_file(ca_files[u]);
   }

   for (u = 0; u < sizeof(ca_paths)/sizeof(ca_paths[0]); u++) {
      if (*ca_paths[u]) {
         Tls_load_certificates_from_path(ca_paths[u]);
      }
   }

   userpath = dStrconcat(dGethomedir(), "/.dillo/certs/", NULL);
   Tls_load_certificates_from_path(userpath);
   dFree(userpath);

   Tls_remove_duplicate_certificates();

   /* Count our trusted certificates */
   u = 0;
   if (cacerts.next) {
      u++;
      for (curr = cacerts.next; curr; curr = curr->next)
         u++;
   } else {
      mbedtls_x509_crt empty;
      mbedtls_x509_crt_init(&empty);

      if (memcmp(&cacerts, &empty, sizeof(mbedtls_x509_crt)))
         u++;
   }
     
   MSG("Trusting %u TLS certificate%s.\n", u, u==1 ? "" : "s");
}

/*
 * Remove the pre-shared key ciphersuites. There are lots of them,
 * and we aren't making any use of them.
 */
static void Tls_remove_psk_ciphersuites()
{
   const mbedtls_ssl_ciphersuite_t *cs_info;
   int *our_ciphers, *q;
   int n = 0;

   const int *default_ciphers = mbedtls_ssl_list_ciphersuites(),
             *p = default_ciphers;

   /* count how many we will want */
   while (*p) {
      cs_info = mbedtls_ssl_ciphersuite_from_id(*p);
      if (!mbedtls_ssl_ciphersuite_uses_psk(cs_info))
         n++;
      p++;
   }
   n++;
   our_ciphers = dNew(int, n);

   /* iterate through again and copy them over */
   p = default_ciphers;
   q = our_ciphers;
   while (*p) {
      cs_info = mbedtls_ssl_ciphersuite_from_id(*p);

      if (!mbedtls_ssl_ciphersuite_uses_psk(cs_info))
         *q++ = *p;
      p++;
   }
   *q = 0;

   mbedtls_ssl_conf_ciphersuites(&ssl_conf, our_ciphers);
}

/*
 * Initialize the mbed TLS library.
 */
void a_Tls_mbedtls_init(void)
{
   int ret;
   char version[128];
   mbedtls_version_get_string_full(version);
   MSG("TLS library: %s\n", version);

    /* As of 2.3.0 in 2016, the 'default' profile allows SHA1, RIPEMD160,
     * and SHA224 (in addition to the stronger ones), and the 'next' profile
     * doesn't allow anything below SHA256. Since we're never going to hear
     * when/if RIPEMD160 and SHA224 are deprecated, and they're obscure enough
     * not to encounter, let's not allow those.
     * These profiles are for certificates, and mbed tls points out that these
     * have nothing to do with hashes during handshakes.
     * Their 'next' profile only allows "Curves at or above 128-bit security
     * level". For now, we follow 'default' and allow all curves.
     */
   static const mbedtls_x509_crt_profile prof = {
    MBEDTLS_X509_ID_FLAG( MBEDTLS_MD_SHA1 ) |
    MBEDTLS_X509_ID_FLAG( MBEDTLS_MD_SHA256 ) |
    MBEDTLS_X509_ID_FLAG( MBEDTLS_MD_SHA384 ) |
    MBEDTLS_X509_ID_FLAG( MBEDTLS_MD_SHA512 ),
    0xFFFFFFF, /* Any PK alg    */
    0xFFFFFFF, /* Any curve     */
    2048,
   };

   mbedtls_ssl_config_init(&ssl_conf);

   mbedtls_ssl_config_defaults(&ssl_conf, MBEDTLS_SSL_IS_CLIENT,
                               MBEDTLS_SSL_TRANSPORT_STREAM,
                               MBEDTLS_SSL_PRESET_DEFAULT);
   mbedtls_ssl_conf_cert_profile(&ssl_conf, &prof);

   /*
    * TLSv1.3 brings some changes, among them, having to call
    * psa_crypto_init(), and a new way of resuming sessions,
    * which is not currently supported by the code here.
    */
#if defined(MBEDTLS_SSL_PROTO_TLS1_3)
   mbedtls_ssl_conf_max_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2);
#endif

   /*
    * There are security concerns surrounding session tickets --
    * wrecking forward security, for instance.
    */
   mbedtls_ssl_conf_session_tickets(&ssl_conf,
                                    MBEDTLS_SSL_SESSION_TICKETS_DISABLED);

   Tls_remove_psk_ciphersuites();

   mbedtls_x509_crt_init(&cacerts);   /* trusted root certificates */
   mbedtls_ctr_drbg_init(&ctr_drbg);  /* Counter mode Deterministic Random Byte
                                       * Generator */
   mbedtls_entropy_init(&entropy);

   if((ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
                                   (unsigned char*)"dillo tls", 9))) {
      ssl_enabled = FALSE;
      MSG_ERR("tls: mbedtls_ctr_drbg_seed() failed. TLS disabled.\n");
      return;
   }

   mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
   mbedtls_ssl_conf_ca_chain(&ssl_conf, &cacerts, NULL);
   mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &ctr_drbg);

   fd_map = dList_new(20);
   servers = dList_new(8);
   cert_authorities = dList_new(12);

   Tls_load_certificates();
}

/*
 * Ordered comparison of servers.
 */
static int Tls_servers_cmp(const void *v1, const void *v2)
{
   const Server_t *s1 = (const Server_t *)v1, *s2 = (const Server_t *)v2;
   int cmp = dStrAsciiCasecmp(s1->hostname, s2->hostname);

   if (!cmp)
      cmp = s1->port - s2->port;
   return cmp;
}
/*
 * Ordered comparison of server with URL.
 */
static int Tls_servers_by_url_cmp(const void *v1, const void *v2)
{
   const Server_t *s = (const Server_t *)v1;
   const DilloUrl *url = (const DilloUrl *)v2;

   int cmp = dStrAsciiCasecmp(s->hostname, URL_HOST(url));

   if (!cmp)
      cmp = s->port - URL_PORT(url);
   return cmp;
}

/*
 * The purpose here is to permit a single initial connection to a server.
 * Once we have the certificate, know whether we like it -- and whether the
 * user accepts it -- HTTP can run through queued sockets as normal.
 *
 * Return: TLS_CONNECT_READY or TLS_CONNECT_NOT_YET or TLS_CONNECT_NEVER.
 */
int a_Tls_mbedtls_connect_ready(const DilloUrl *url)
{
   Server_t *s;
   int ret = TLS_CONNECT_READY;

   dReturn_val_if_fail(ssl_enabled, TLS_CONNECT_NEVER);

   if ((s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp))) {
      if (s->cert_status == CERT_STATUS_RECEIVING)
         ret = TLS_CONNECT_NOT_YET;
      else if (s->cert_status == CERT_STATUS_BAD)
         ret = TLS_CONNECT_NEVER;

      if (s->cert_status == CERT_STATUS_NONE)
         s->cert_status = CERT_STATUS_RECEIVING;
   } else {
      s = dNew(Server_t, 1);

      s->hostname = dStrdup(URL_HOST(url));
      s->port = URL_PORT(url);
      s->cert_status = CERT_STATUS_RECEIVING;
      dList_insert_sorted(servers, s, Tls_servers_cmp);
   }
   return ret;
}

static int Tls_cert_status(const DilloUrl *url)
{
   Server_t *s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp);

   return s ? s->cert_status : CERT_STATUS_NONE;
}

/*
 * Did we find problems with the certificate, and did the user proceed to
 * reject the connection?
 */
static int Tls_user_said_no(const DilloUrl *url)
{
   return Tls_cert_status(url) == CERT_STATUS_BAD;
}

/*
 * Did everything seem proper with the certificate -- no warnings to
 * click through?
 */
int a_Tls_mbedtls_certificate_is_clean(const DilloUrl *url)
{
   return Tls_cert_status(url) == CERT_STATUS_CLEAN;
}

#if 0
/*
 * Print certificate and its chain of issuer certificates.
 */
static void Tls_print_cert_chain(const mbedtls_x509_crt *cert)
{
   /* print for first connection to server */
   const mbedtls_x509_crt *last_cert;
   const uint_t buflen = 2048;
   char buf[buflen];
   int key_bits;
   const char *sigalg;

   while (cert) {
      if (cert->sig_md == MBEDTLS_MD_SHA1) {
         MSG_WARN("In 2015, browsers have begun to deprecate SHA1 "
                  "certificates.\n");
      }

      if (mbedtls_oid_get_sig_alg_desc(&cert->sig_oid, &sigalg))
         sigalg = "(??" ")";

      key_bits = mbedtls_pk_get_bitlen(&cert->pk);
      mbedtls_x509_dn_gets(buf, buflen, &cert->subject);
      MSG("%d-bit %s: %s\n", key_bits, sigalg, buf);

      last_cert = cert;
      cert = cert->next;
   }
   if (last_cert) {
      mbedtls_x509_dn_gets(buf, buflen, &last_cert->issuer);
      MSG("root: %s\n", buf);
   }
}
#endif

/*
 * Generate dialog msg for expired cert.
 */
static void Tls_cert_expired(const mbedtls_x509_crt *cert, Dstr *ds)
{
   const mbedtls_x509_time *date = &cert->valid_to;

   dStr_sprintfa(ds,"Certificate expired at: %04d/%02d/%02d %02d:%02d:%02d.\n",
                 date->year, date->mon, date->day, date->hour, date->min,
                 date->sec);
}

/*
 * Generate dialog msg when certificate is not for this host.
 */
static void Tls_cert_cn_mismatch(const mbedtls_x509_crt *cert, Dstr *ds)
{
   const uint_t buflen = 2048;
   char cert_info_buf[buflen];
   char *san, *s;

   dStr_append(ds, "This host is not one of the hostnames listed on the TLS "
                   "certificate that it sent");
   /*
    *
    * Taking the human-readable certificate info and scraping it is brittle
    * and horrible, but the alternative is to mimic
    * x509_info_subject_alt_name(), an option that seems equally brittle and
    * horrible.
    *
    * Once I find a case where SAN isn't used, I can add code to work with
    * the subject field as well.
    *
    */
   mbedtls_x509_crt_info(cert_info_buf, buflen, "", cert);

   if ((san = strstr(cert_info_buf, "subject alt name  : "))) {
      san += 20;
      s = strchr(san, '\n');
      if (s) {
         *s = '\0';
         dStr_sprintfa(ds, " (%s)", san);
      }
   }
   dStr_append(ds, ".\n");
}

/*
 * Generate dialog msg when certificate is not trusted.
 */
static void Tls_cert_trust_chain_failed(const mbedtls_x509_crt *cert, Dstr *ds)
{
   const uint_t buflen = 2048;
   char buf[buflen];

   while (cert->next)
      cert = cert->next;
   mbedtls_x509_dn_gets(buf, buflen, &cert->issuer);

   dStr_sprintfa(ds, "Couldn't reach any trusted root certificate from "
                     "supplied certificate. The issuer at the end of the "
                     "chain was: \"%s\"\n", buf);
}

/*
 * Generate dialog msg when certificate start date is in the future.
 */
static void Tls_cert_not_valid_yet(const mbedtls_x509_crt *cert, Dstr *ds)
{
   const mbedtls_x509_time *date = &cert->valid_to;

   dStr_sprintfa(ds, "Certificate validity begins in the future at: "
                     "%04d/%02d/%02d %02d:%02d:%02d.\n",
                     date->year, date->mon, date->day, date->hour, date->min,
                     date->sec);
}

/*
 * Generate dialog msg when certificate hash algorithm is not accepted.
 */
static void Tls_cert_bad_hash(const mbedtls_x509_crt *cert, Dstr *ds)
{
#if MBEDTLS_VERSION_NUMBER < 0x03000000
   mbedtls_md_type_t md = cert->sig_md;
#else
   mbedtls_md_type_t md = cert->MBEDTLS_PRIVATE(sig_md);
#endif
   const char *hash = (md == MBEDTLS_MD_MD5) ? "MD5" :
                      (md == MBEDTLS_MD_SHA1) ? "SHA1" :
                      (md == MBEDTLS_MD_SHA224) ? "SHA224" :
                      (md == MBEDTLS_MD_RIPEMD160) ? "RIPEMD160" :
                      (md == MBEDTLS_MD_SHA256) ? "SHA256" :
                      (md == MBEDTLS_MD_SHA384) ? "SHA384" :
                      (md == MBEDTLS_MD_SHA512) ? "SHA512" :
#if MBEDTLS_VERSION_NUMBER < 0x03000000
/* In version 3, these are removed: */
                      (md == MBEDTLS_MD_MD4) ? "MD4" :
                      (md == MBEDTLS_MD_MD2) ? "MD2" :
#endif
                      "Unrecognized";

   dStr_sprintfa(ds, "This certificate's hash algorithm is not accepted "
                     "(%s).\n", hash);
}

/*
 * Generate dialog msg when public key algorithm (RSA, ECDSA) is not accepted.
 */
static void Tls_cert_bad_pk_alg(const mbedtls_x509_crt *cert, Dstr *ds)
{
   const char *type_str = mbedtls_pk_get_name(&cert->pk);

   dStr_sprintfa(ds, "This certificate's public key algorithm is not accepted "
                     "(%s).\n", type_str);
}

/*
 * Generate dialog msg when the public key is not acceptable. As of 2016,
 * this was triggered by RSA keys below 2048 bits, if I recall correctly.
 */
static void Tls_cert_bad_key(const mbedtls_x509_crt *cert, Dstr *ds) {
   int key_bits = mbedtls_pk_get_bitlen(&cert->pk);
   const char *type_str = mbedtls_pk_get_name(&cert->pk);

   dStr_sprintfa(ds, "This certificate's key is not accepted, which generally "
                     "means it's too weak (%d-bit %s).\n", key_bits, type_str);
}

/*
 * Make a dialog msg containing warnings about problems with the certificate.
 */
static char *Tls_make_bad_cert_msg(const mbedtls_x509_crt *cert,uint32_t flags)
{
   static const struct certerr {
      int val;
      void (*cert_err_fn)(const mbedtls_x509_crt *cert, Dstr *ds);
   } cert_error [] = {
      { MBEDTLS_X509_BADCERT_EXPIRED, Tls_cert_expired},
      { MBEDTLS_X509_BADCERT_CN_MISMATCH, Tls_cert_cn_mismatch},
      { MBEDTLS_X509_BADCERT_NOT_TRUSTED, Tls_cert_trust_chain_failed},
      { MBEDTLS_X509_BADCERT_FUTURE, Tls_cert_not_valid_yet},
      { MBEDTLS_X509_BADCERT_BAD_MD, Tls_cert_bad_hash},
      { MBEDTLS_X509_BADCERT_BAD_PK, Tls_cert_bad_pk_alg},
      { MBEDTLS_X509_BADCERT_BAD_KEY, Tls_cert_bad_key}
   };
   const uint_t ncert_errors = sizeof(cert_error) /sizeof(cert_error[0]);
   char *ret;
   Dstr *ds = dStr_new(NULL);
   uint_t u;

   for (u = 0; u < ncert_errors; u++) {
      if (flags & cert_error[u].val) {
         flags &= ~cert_error[u].val;
         cert_error[u].cert_err_fn(cert, ds);
      }
   }
   if (flags)
      dStr_sprintfa(ds, "Unknown certificate error(s): flag value 0x%04x",
                    flags);
   ret = ds->str;
   dStr_free(ds, 0);
   return ret;
}

static int Tls_cert_auth_cmp(const void *v1, const void *v2)
{
   const CertAuth_t *c1 = (CertAuth_t *)v1, *c2 = (CertAuth_t *)v2;

   return strcmp(c1->name, c2->name);
}

static int Tls_cert_auth_cmp_by_name(const void *v1, const void *v2)
{
   const CertAuth_t *c = (CertAuth_t *)v1;
   const char *name = (char *)v2;

   return strcmp(c->name, name);
}

/*
 * Keep account of on whose authority we are trusting servers.
 */
static void Tls_update_cert_authorities_data(const mbedtls_x509_crt *cert,
                                             Server_t *srv)
{
   const uint_t buflen = 512;
   char buf[buflen];
   const mbedtls_x509_crt *last = cert;

   while (last->next)
      last = last->next;

   mbedtls_x509_dn_gets(buf, buflen, &last->issuer);

   CertAuth_t *ca = dList_find_custom(cert_authorities, buf,
                                      Tls_cert_auth_cmp_by_name);
   if (!ca) {
      ca = dNew(CertAuth_t, 1);
      ca->name = dStrdup(buf);
      ca->servers = dList_new(8);
      dList_insert_sorted(cert_authorities, ca, Tls_cert_auth_cmp);
   }
   dList_append(ca->servers, srv);
}

/*
 * Examine the certificate, and, if problems are detected, ask the user what
 * to do.
 * Return: -1 if connection should be canceled, or 0 if it should continue.
 */
static int Tls_examine_certificate(mbedtls_ssl_context *ssl, Server_t *srv)
{
   const mbedtls_x509_crt *cert;
   uint32_t st;
   int choice = -1, ret = -1;
   char *title = dStrconcat("Dillo TLS security warning: ",srv->hostname,NULL);

   cert = mbedtls_ssl_get_peer_cert(ssl);
   if (cert == NULL){
      /* Inform user that remote system cannot be trusted */
      choice = a_Dialog_choice(title,
         "No certificate received from this site. Can't verify who it is.",
         "Continue", "Cancel", NULL);

      /* Abort on anything but "Continue" */
      if (choice == 1){
         ret = 0;
      }
   } else {
      /* check the certificate */
      st = mbedtls_ssl_get_verify_result(ssl);
      if (st == 0) {
         if (srv->cert_status == CERT_STATUS_RECEIVING) {
            /* first connection to server */
#if 0
            Tls_print_cert_chain(cert);
#endif
            Tls_update_cert_authorities_data(cert, srv);
         }
         ret = 0;
      } else if (st == 0xFFFFFFFF) {
         /* "result is not available (eg because the handshake was aborted too
          * early)" is what the documentation says. Maybe it's only what
          * happens if you call get_verify_result() too early or when the
          * handshake failed. But just in case...
          */
         MSG_ERR("mbedtls_ssl_get_verify_result: result is not available");
      } else {
         char *dialog_warning_msg = Tls_make_bad_cert_msg(cert, st);

         choice = a_Dialog_choice(title, dialog_warning_msg, "Continue",
                                  "Cancel", NULL);
         if (choice == 1) {
            ret = 0;
         }
         dFree(dialog_warning_msg);
      }
   }
   dFree(title);

   if (choice == -1) {
      srv->cert_status = CERT_STATUS_CLEAN;          /* no warning popups */
   } else if (choice == 1) {
      srv->cert_status = CERT_STATUS_USER_ACCEPTED;  /* clicked Continue */
   } else {
      /* 2 for Cancel, or 0 when window closed. */
      srv->cert_status = CERT_STATUS_BAD;
   }
   return ret;
}

/*
 * If the connection was closed before we got the certificate, we need to
 * reset state so that we'll try again.
 */
void a_Tls_mbedtls_reset_server_state(const DilloUrl *url)
{
   if (servers) {
      Server_t *s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp);

      if (s && s->cert_status == CERT_STATUS_RECEIVING)
         s->cert_status = CERT_STATUS_NONE;
   }
}

/*
 * Close an open TLS connection.
 */
static void Tls_close_by_key(int connkey)
{
   Conn_t *c;

   if ((c = a_Klist_get_data(conn_list, connkey))) {
      a_Tls_mbedtls_reset_server_state(c->url);
      if (c->connecting) {
         a_IOwatch_remove_fd(c->fd, -1);
         dClose(c->fd);
      }
      mbedtls_ssl_close_notify(c->ssl);
      mbedtls_ssl_free(c->ssl);
      dFree(c->ssl);

      a_Url_free(c->url);
      Tls_fd_map_remove_entry(c->fd);
      a_Klist_remove(conn_list, connkey);
      dFree(c);
   }
}

/*
 * Print a message about the fatal alert.
 *
 * The values have gaps, and a few are never fatal error values, and some may
 * never be sent to clients, but let's go ahead and translate every value that
 * we recognize.
 */
static void Tls_fatal_error_msg(int error_type)
{
   const char *errmsg;

   if (error_type == MBEDTLS_SSL_ALERT_MSG_CLOSE_NOTIFY)
      errmsg = "close notify";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_UNEXPECTED_MESSAGE)
      errmsg = "unexpected message received";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_BAD_RECORD_MAC)
      errmsg = "record received with incorrect MAC";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_DECRYPTION_FAILED) {
      /* last used in TLS 1.1 */
      errmsg = "decryption failed";
   } else if (error_type == MBEDTLS_SSL_ALERT_MSG_RECORD_OVERFLOW)
      errmsg = "\"A TLSCiphertext record was received that had a length more "
               "than 2^14+2048 bytes, or a record decrypted to a TLSCompressed"
               " record with more than 2^14+1024 bytes.\"";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_DECOMPRESSION_FAILURE)
      errmsg = "\"decompression function received improper input\"";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_HANDSHAKE_FAILURE)
      errmsg = "\"sender was unable to negotiate an acceptable set of security"
               " parameters given the options available\"";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_NO_CERT)
      errmsg = "no cert (an obsolete alert last used in SSL3)";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_BAD_CERT)
      errmsg = "bad certificate";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT)
      errmsg = "certificate of unsupported type";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_CERT_REVOKED)
      errmsg = "certificate revoked by its signer";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_CERT_EXPIRED)
      errmsg = "certificate expired or not currently valid";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_CERT_UNKNOWN)
      errmsg = "certificate error of an unknown sort";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_ILLEGAL_PARAMETER)
      errmsg = "illegal parameter in handshake";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_UNKNOWN_CA)
      errmsg = "unknown CA";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_ACCESS_DENIED)
      errmsg = "access denied";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_DECODE_ERROR)
      errmsg = "decode error";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_DECRYPT_ERROR)
      errmsg = "decrypt error";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_EXPORT_RESTRICTION) {
      /* last used in TLS 1.0 */
      errmsg = "export restriction";
   } else if (error_type == MBEDTLS_SSL_ALERT_MSG_PROTOCOL_VERSION)
      errmsg = "protocol version is recognized but not supported";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_INSUFFICIENT_SECURITY)
      errmsg = "server requires ciphers more secure than those supported by "
               "the client";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_INTERNAL_ERROR)
      errmsg = "internal error (not the client's fault)";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_INAPROPRIATE_FALLBACK)
      errmsg = "inappropriate fallback";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_USER_CANCELED)
      errmsg = "\"handshake is being canceled for some reason unrelated to a "
               "protocol failure\"";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_NO_RENEGOTIATION)
      errmsg = "no renegotiation";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_EXT)
      errmsg = "unsupported ext";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_UNRECOGNIZED_NAME)
      errmsg = "unrecognized name";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_UNKNOWN_PSK_IDENTITY)
      errmsg = "unknown psk identity";
   else if (error_type == MBEDTLS_SSL_ALERT_MSG_NO_APPLICATION_PROTOCOL)
      errmsg = "no application protocol";
   else errmsg = "unknown alert value";

   MSG_WARN("mbedtls_ssl_handshake() received TLS fatal alert %d (%s)\n",
            error_type, errmsg);
}

/*
 * Connect, set a callback if it's still not completed. If completed, check
 * the certificate and report back to http.
 */
static void Tls_handshake(int fd, int connkey)
{
   int ret;
   bool_t ongoing = FALSE, failed = TRUE;
   Conn_t *conn;

   if (!(conn = a_Klist_get_data(conn_list, connkey))) {
      MSG("Tls_connect: conn for fd %d not valid\n", fd);
      return;
   }

#if MBEDTLS_VERSION_NUMBER < 0x03000000
   int ssl_state = conn->ssl->state;
#else
   int ssl_state = conn->ssl->MBEDTLS_PRIVATE(state);
#endif
   if (ssl_state != MBEDTLS_SSL_HANDSHAKE_OVER) {
      ret = mbedtls_ssl_handshake(conn->ssl);

      if (ret == MBEDTLS_ERR_SSL_WANT_READ ||
          ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
         int want = ret == MBEDTLS_ERR_SSL_WANT_READ ? DIO_READ : DIO_WRITE;

         _MSG("iowatching fd %d for tls -- want %s\n", fd,
             ret == MBEDTLS_ERR_SSL_WANT_READ ? "read" : "write");
         a_IOwatch_remove_fd(fd, -1);
         a_IOwatch_add_fd(fd, want, Tls_handshake_cb, INT2VOIDP(connkey));
         ongoing = TRUE;
         failed = FALSE;
      } else if (ret == 0) {
         Server_t *srv = dList_find_sorted(servers, conn->url,
                                           Tls_servers_by_url_cmp);

         if (srv->cert_status == CERT_STATUS_RECEIVING) {
            /* Making first connection with the server. Show cipher used. */
            mbedtls_ssl_context *ssl = conn->ssl;
            const char *version = mbedtls_ssl_get_version(ssl),
                       *cipher = mbedtls_ssl_get_ciphersuite(ssl);

            MSG("%s", URL_AUTHORITY(conn->url));
            if (URL_PORT(conn->url) != URL_HTTPS_PORT)
               MSG(":%d", URL_PORT(conn->url));
            MSG(" %s, cipher %s\n", version, cipher);
         }
         if (srv->cert_status == CERT_STATUS_USER_ACCEPTED ||
             (Tls_examine_certificate(conn->ssl, srv) != -1)) {
            failed = FALSE;
         }
      } else if (ret == MBEDTLS_ERR_NET_SEND_FAILED) {
         MSG("mbedtls_ssl_handshake() send failed. Server may not be accepting"
             " connections.\n");
      } else if (ret == MBEDTLS_ERR_NET_CONNECT_FAILED) {
         MSG("mbedtls_ssl_handshake() connect failed.\n");
      } else if (ret == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE) {
         /* Paul Bakker, the mbed tls guy, says "beware, this might change in
          * future versions" and "ssl->in_msg[1] is not going to change anytime
          * soon, unless there are radical changes". It seems to be the best of
          * the alternatives.
          */
#if MBEDTLS_VERSION_NUMBER < 0x03000000
         Tls_fatal_error_msg(conn->ssl->in_msg[1]);
#else
         Tls_fatal_error_msg(conn->ssl->MBEDTLS_PRIVATE(in_msg[1]));
#endif
      } else if (ret == MBEDTLS_ERR_SSL_INVALID_RECORD) {
         MSG("mbedtls_ssl_handshake() failed upon receiving 'an invalid "
             "record'.\n");
      } else if (ret == MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE) {
         MSG("mbedtls_ssl_handshake() failed: 'The requested feature is not "
             "available.'\n");
#if MBEDTLS_VERSION_NUMBER < 0x03000000
      } else if (ret == MBEDTLS_ERR_SSL_BAD_HS_SERVER_KEY_EXCHANGE) {
         MSG("mbedtls_ssl_handshake() failed: 'Processing of the "
             "ServerKeyExchange handshake message failed.'\n");
#endif
      } else if (ret == MBEDTLS_ERR_SSL_CONN_EOF) {
         MSG("mbedtls_ssl_handshake() failed: Read EOF. Connection closed by "
             "server.\n");
      } else {
         MSG("mbedtls_ssl_handshake() failed with error -0x%04x\n", -ret);
      }
   }

   /*
    * If there were problems with the certificate, the connection may have
    * been closed by the server if the user responded too slowly to a popup.
    */

   if (!ongoing) {
      if (a_Klist_get_data(conn_list, connkey)) {
         conn->connecting = FALSE;
         if (failed) {
            Tls_close_by_key(connkey);
         }
         a_IOwatch_remove_fd(fd, -1);
         a_Http_connect_done(fd, failed ? FALSE : TRUE);
      } else {
         MSG("Connection disappeared. Too long with a popup popped up?\n");
      }
   }
}

static void Tls_handshake_cb(int fd, void *vconnkey)
{
   Tls_handshake(fd, VOIDP2INT(vconnkey));
}

/*
 * Make TLS connection over a connect()ed socket.
 */
void a_Tls_mbedtls_connect(int fd, const DilloUrl *url)
{
   mbedtls_ssl_context *ssl = dNew0(mbedtls_ssl_context, 1);
   bool_t success = TRUE;
   int connkey = -1;
   int ret;

   if (!ssl_enabled)
      success = FALSE;

   if (success && Tls_user_said_no(url)) {
      success = FALSE;
   }

   if (success && (ret = mbedtls_ssl_setup(ssl, &ssl_conf))) {
      MSG("mbedtls_ssl_setup failed %d\n", ret);
      success = FALSE;
   }

   /* assign TLS connection to this file descriptor */
   if (success) {
      Conn_t *conn = Tls_conn_new(fd, url, ssl);
      connkey = Tls_make_conn_key(conn);
      mbedtls_ssl_set_bio(ssl, &conn->fd, mbedtls_net_send, mbedtls_net_recv,
                          NULL);
   }

   if (success && (ret = mbedtls_ssl_set_hostname(ssl, URL_HOST(url)))) {
      MSG("mbedtls_ssl_set_hostname failed %d\n", ret);
      success = FALSE;
   }

   if (!success) {
      a_Tls_mbedtls_reset_server_state(url);
      a_Http_connect_done(fd, success);
   } else {
      Tls_handshake(fd, connkey);
   }
}

/*
 * Read data from an open TLS connection.
 */
int a_Tls_mbedtls_read(void *conn, void *buf, size_t len)
{
   Conn_t *c = (Conn_t*)conn;
   int ret = mbedtls_ssl_read(c->ssl, buf, len);

   if (ret < 0) {
      if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
         /* treat it as EOF */
         ret = 0;
      } else if (ret == MBEDTLS_ERR_SSL_WANT_READ) {
         ret = -1;
         errno = EAGAIN; /* already happens to be set, but let's make sure */
      } else if (ret == MBEDTLS_ERR_NET_CONN_RESET) {
         MSG("READ failed: TLS connection reset by server.\n");
      } else {
         MSG("READ failed with -0x%04x: an mbed tls error.\n", -ret);
      }
   }
   return ret;
}

/*
 * Write data to an open TLS connection.
 */
int a_Tls_mbedtls_write(void *conn, void *buf, size_t len)
{
   Conn_t *c = (Conn_t*)conn;
   int ret = mbedtls_ssl_write(c->ssl, buf, len);

   if (ret < 0) {
      MSG("WRITE failed with -0x%04x: an mbed tls error\n", -ret);
   }
   return ret;
}

void a_Tls_mbedtls_close_by_fd(int fd)
{
   FdMapEntry_t *fme = dList_find_custom(fd_map, INT2VOIDP(fd),
                                         Tls_fd_map_cmp);

   if (fme) {
      Tls_close_by_key(fme->connkey);
   }
}

static void Tls_cert_authorities_print_summary()
{
   const int ca_len = dList_length(cert_authorities);
   Dstr *ds = dStr_new("");
   int i, j;

   if (ca_len)
      dStr_append(ds, "TLS: Certificate chain roots during this session:\n");

   for (i = 0; i < ca_len; i++) {
      CertAuth_t *ca = (CertAuth_t *)dList_nth_data(cert_authorities, i);
      const int servers_len = ca->servers ? dList_length(ca->servers) : 0;
      char *ca_name = strstr(ca->name, "CN=");

      if (!ca_name)
         ca_name = strstr(ca->name, "OU=");

      if (ca_name)
         ca_name += 3;
      else
         ca_name = ca->name;
      dStr_sprintfa(ds, "- %s for: ", ca_name);

      for (j = 0; j < servers_len; j++) {
         Server_t *s = dList_nth_data(ca->servers, j);
         bool_t ipv6 = a_Url_host_type(s->hostname) == URL_HOST_IPV6;

         dStr_sprintfa(ds, "%s%s%s", ipv6?"[":"", s->hostname, ipv6?"]":"");
         if (s->port != URL_HTTPS_PORT)
            dStr_sprintfa(ds, ":%d", s->port);
         dStr_append_c(ds, ' ');
      }
      dStr_append_c(ds, '\n');
   }
   MSG("%s", ds->str);
   dStr_free(ds, 1);
}

/*
 * Free mbed tls's chain of certificates and free our data on which root
 * certificates caused us to trust which servers.
 */
static void Tls_cert_authorities_freeall()
{
   if (cacerts.next)
      mbedtls_x509_crt_free(cacerts.next);

   if (cert_authorities) {
      CertAuth_t *ca;
      int i, n = dList_length(cert_authorities);

      for (i = 0; i < n; i++) {
         ca = (CertAuth_t *) dList_nth_data(cert_authorities, i);
         dFree(ca->name);
         if (ca->servers)
            dList_free(ca->servers);
         dFree(ca);
      }
      dList_free(cert_authorities);
   }
}

static void Tls_servers_freeall()
{
   if (servers) {
      Server_t *s;
      int i, n = dList_length(servers);

      for (i = 0; i < n; i++) {
         s = (Server_t *) dList_nth_data(servers, i);
         dFree(s->hostname);
         dFree(s);
      }
      dList_free(servers);
   }
}

static void Tls_fd_map_remove_all()
{
   if (fd_map) {
      FdMapEntry_t *fme;
      int i, n = dList_length(fd_map);

      for (i = 0; i < n; i++) {
         fme = (FdMapEntry_t *) dList_nth_data(fd_map, i);
         dFree(fme);
      }
      dList_free(fd_map);
   }
}

/*
 * Clean up
 */
void a_Tls_mbedtls_freeall(void)
{
   if (prefs.show_msg)
      Tls_cert_authorities_print_summary();

   Tls_fd_map_remove_all();
   Tls_cert_authorities_freeall();
   Tls_servers_freeall();
}
