/***************************************************************************
 *
 * Copyright (c) 2000, 2001 BalaBit IT Ltd, Budapest, Hungary
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: ssl.c,v 1.1.2.1.2.4 2001/09/19 19:22:17 bazsi Exp $
 *
 ***************************************************************************/

/* interface between Zorp and the SSL library */

#include <zorp/ssl.h>
#include <zorp/log.h>
#include <zorp/giossl.h>

#include <glib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <openssl/err.h>

static int ssl_initialized = 0;

static GStaticMutex *ssl_mutexes;
static int mutexnum;

gchar *
z_ssl_get_error_str(gchar *buf, int buflen)
{
  const char *ls, *fs, *rs;
  unsigned long e, l, f, r;

  e = ERR_get_error();

  l = ERR_GET_LIB(e);
  f = ERR_GET_FUNC(e);
  r = ERR_GET_REASON(e);

  ls = ERR_lib_error_string(e);
  fs = ERR_func_error_string(e);
  rs = ERR_reason_error_string(e);

  g_snprintf(buf, buflen, "error:%08lX:%s:lib(%lu):%s:func(%lu):%s:reason(%lu)", e, ls, l, fs, f, rs, r);
  return buf;
}


static void
z_ssl_locking_callback(int mode, int n, const char *file, int line)
{
  z_enter();
  if (n >= mutexnum)
    {
      /*LOG
        This message indicates a broken OpenSSL library, since it tries
        to use more mutexes than it originally requested.
       */
      z_log(NULL, CORE_ERROR, 3, "SSL requested an out of bounds mutex; max='%d', n='%d'", mutexnum, n);
    }
  if (mode & CRYPTO_LOCK)
    {
      z_trace(NULL, "Mutex %d locked", n);
      g_static_mutex_lock(&ssl_mutexes[n]);
    }
  else
    {
      z_trace(NULL,  "Mutex %d unlocked", n);
      g_static_mutex_unlock(&ssl_mutexes[n]);
    }
  z_leave();
}

void
z_ssl_init_mutexes(void)
{
  z_enter();
  mutexnum = CRYPTO_num_locks();
  ssl_mutexes = g_new0(GStaticMutex, mutexnum);
  
  CRYPTO_set_locking_callback(z_ssl_locking_callback);
  z_leave();
}

void
z_ssl_init()
{
  z_enter();
  if (ssl_initialized)
    {
      z_leave();
      return;
    }
  SSL_library_init();
  SSL_load_error_strings();

  z_ssl_init_mutexes();
  
  ssl_initialized = 1;
  z_leave();
}

void
z_ssl_destroy(void)
{
  ssl_initialized = 0;
  g_free(ssl_mutexes);
}

static int
z_ssl_x509_store_lookup(X509_STORE *store, int type,
                        X509_NAME *name, X509_OBJECT *obj)
{
  X509_STORE_CTX store_ctx;
  int rc;

  z_enter();
  X509_STORE_CTX_init(&store_ctx, store, NULL, NULL);
  rc = X509_STORE_get_by_subject(&store_ctx, type, name, obj);
  X509_STORE_CTX_cleanup(&store_ctx);
  z_leave();
  return rc;
}

int
z_ssl_verify_crl(int ok, 
                 X509 *xs,
                 X509_STORE_CTX *ctx,
                 X509_STORE *crl_store, 
                 gchar *session_id)
{
  X509_OBJECT obj;
  X509_NAME *subject, *issuer;
  X509_CRL *crl;
  char subject_name[512], issuer_name[512];
  int rc;

  z_enter(); 

  subject = X509_get_subject_name(xs);
  X509_NAME_oneline(subject, subject_name, sizeof(subject_name));
  
  issuer = X509_get_issuer_name(xs);
  X509_NAME_oneline(issuer, issuer_name, sizeof(issuer_name));
 
  memset((char *)&obj, 0, sizeof(obj));
  
  rc = z_ssl_x509_store_lookup(crl_store, X509_LU_CRL, subject, &obj);
  
  crl = obj.data.crl;
  if (rc > 0 && crl != NULL)
    {

      /*
       * Log information about CRL
       * (A little bit complicated because of ASN.1 and BIOs...)
       */
      BIO *bio;
      char *cp;
      int n, i;

      bio = BIO_new(BIO_s_mem());
      BIO_printf(bio, "lastUpdate='");
      ASN1_UTCTIME_print(bio, X509_CRL_get_lastUpdate(crl));
      BIO_printf(bio, "', nextUpdate='");
      ASN1_UTCTIME_print(bio, X509_CRL_get_nextUpdate(crl));
      BIO_printf(bio, "'");
      n = BIO_pending(bio);
      
      cp = alloca(n+1);
      n = BIO_read(bio, cp, n);
      cp[n] = 0;
      BIO_free(bio);

      /*LOG
        This is an informational message which informs you about checking
        the given CRL for certificate revocations.
       */      
      z_log(session_id, CORE_DEBUG, 4, "Verifying CA CRL; issuer='%s', %s", subject_name, cp);

      if (X509_CRL_verify(crl, X509_get_pubkey(xs)) <= 0)
        {
          /*LOG
            This message indicates an invalid Certificate Revocation List (CRL),
            because it is not signed by the CA it is said to belong to.
           */
          z_log(session_id, CORE_DEBUG, 4, "Invalid signature on CRL; issuer='%s'", subject_name);
          X509_STORE_CTX_set_error(ctx, X509_V_ERR_CRL_SIGNATURE_FAILURE);
          X509_OBJECT_free_contents(&obj);
          z_leave();
          return FALSE;
        }

      i = X509_cmp_current_time(X509_CRL_get_nextUpdate(crl));
      if (i == 0)
        {
          /*LOG
            This message indicates an invalid Certificate Revocation List (CRL),
            because it has an invalid nextUpdate field.
           */
          z_log(session_id, CORE_DEBUG, 4, "CRL has invalid nextUpdate field; issuer='%s'", subject_name);
          
          X509_STORE_CTX_set_error(ctx, X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD);
          X509_OBJECT_free_contents(&obj);
          z_leave();
          return FALSE;
        }
      if (i < 0)
        {
          /*LOG
            This message indicates an invalid Certificate Revocation List (CRL),
            because it is expired.
           */
          z_log(session_id, CORE_DEBUG, 4, "CRL is expired; issuer='%s'", subject_name);
          X509_STORE_CTX_set_error(ctx, X509_V_ERR_CRL_HAS_EXPIRED);
          X509_OBJECT_free_contents(&obj);
          z_leave();
          return FALSE;
        }
      X509_OBJECT_free_contents(&obj);
    }

  memset((char *)&obj, 0, sizeof(obj));
  
  rc = z_ssl_x509_store_lookup(crl_store, X509_LU_CRL, issuer, &obj);
  
  crl = obj.data.crl;
  if (rc > 0 && crl != NULL)
    {
      X509_REVOKED *revoked;
      long serial;
      int i, n;
      
      n = sk_X509_REVOKED_num(X509_CRL_get_REVOKED(crl));
      for (i = 0; i < n; i++)
        {
          revoked = sk_X509_REVOKED_value(X509_CRL_get_REVOKED(crl), i);
          if (ASN1_INTEGER_cmp(revoked->serialNumber, X509_get_serialNumber(xs)) == 0)
            {
              serial = ASN1_INTEGER_get(revoked->serialNumber);
              /*LOG
                This message indicates that a certificate verification failed,
                because the issuing CA revoked it in its Certificate Revocation
                List.
               */
              z_log(session_id, CORE_DEBUG, 4, "Certificate revoked by CRL; issuer='%s', serial=0x%lX",
                    issuer_name, serial);

              X509_OBJECT_free_contents(&obj);
              z_leave();
              return FALSE;
            }
        }
      X509_OBJECT_free_contents(&obj);
    }
  z_leave();
  return ok;
}
                                                  
int
z_ssl_verify_callback(int ok, X509_STORE_CTX *ctx)
{
  ZSSLSession *verify_data;
  SSL *ssl;
  X509 *xs;
  char subject_name[512], issuer_name[512];
  int errnum;
  int errdepth;

  z_enter();
  
  ssl = (SSL *) X509_STORE_CTX_get_app_data(ctx);
  verify_data = (ZSSLSession *) SSL_get_app_data(ssl);
  
  xs = X509_STORE_CTX_get_current_cert(ctx);
  errnum = X509_STORE_CTX_get_error(ctx);
  errdepth = X509_STORE_CTX_get_error_depth(ctx);

  X509_NAME_oneline(X509_get_subject_name(xs), subject_name, sizeof(subject_name));
  X509_NAME_oneline(X509_get_issuer_name(xs),  issuer_name, sizeof(issuer_name));

  /*LOG
    This message indicates that the given certificate is being checked against
    validity.
   */
  z_log(verify_data->session_id, CORE_DEBUG, 6, "Verifying certificate; depth='%d', subject='%s', issuer='%s'", 
        errdepth, subject_name, issuer_name);

  if ((verify_data->verify_type == Z_SSL_VERIFY_REQUIRED_UNTRUSTED || 
       verify_data->verify_type == Z_SSL_VERIFY_OPTIONAL) &&
      (errnum == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT || 
       errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN || 
       errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY || 
       errnum == X509_V_ERR_CERT_UNTRUSTED || 
       errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE))
    {
      /*LOG
        This message indicates that certificate verification failed, but
        it is ignored since the administrator requested optional certificate
        verification.
       */
      z_log(verify_data->session_id, CORE_DEBUG, 4, "Untrusted certificate, ignoring because verification is not required; subject='%s', issuer='%s'", 
            subject_name, issuer_name);
      ok = TRUE;
    }
 
  if (ok && verify_data->crl_store)
    {
      ok = z_ssl_verify_crl(ok, xs, ctx, verify_data->crl_store, verify_data->session_id);
      if (!ok)
        {
          errnum = X509_STORE_CTX_get_error(ctx);
          /*LOG
            This message indicates that a certificate verification failed,
            because the issuing CA revoked it in its Certificate Revocation
            List.
           */
          z_log(verify_data->session_id, CORE_DEBUG, 4, "Certificate is revoked; subject='%s', issuer='%s'", 
                subject_name, issuer_name);
        }
    }
  
  if (ok && verify_data->verify_depth != -1)
    {
      if (errdepth > verify_data->verify_depth)
        {
          /*LOG
            Certificate was verified successfully, but the length of the
            verification chain is over configured limits.
           */
          z_log(verify_data->session_id, CORE_DEBUG, 4, "Certificate chain is too long; subject='%s', issuer='%s' depth='%d' maxdepth='%d'", subject_name, issuer_name, errdepth, verify_data->verify_depth);
          errnum = X509_V_ERR_CERT_CHAIN_TOO_LONG;
          ok = FALSE;
        }
    }

  if (!ok)
    {
      /*LOG
        This message indicates that certificate was invalid, and the connection
        is being denied.
       */
      z_log(verify_data->session_id, CORE_ERROR, 3, "Certificate verification error; subject='%s', issuer='%s', errcode='%d', error='%s'", subject_name, issuer_name, errnum, X509_verify_cert_error_string(errnum));
    }

  z_leave();
  return ok;
}

X509_STORE *
z_ssl_crl_store_create(gchar *crl_path)
{
  X509_STORE *store;
  X509_LOOKUP *lookup;

  z_enter();        
  if ((store = X509_STORE_new()) == NULL)
    {
      z_leave();
      return NULL;
    }
  if (crl_path != NULL)
    {
      if ((lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir())) == NULL)
        {
          X509_STORE_free(store);
          z_leave();
          return NULL;
        }
      X509_LOOKUP_add_dir(lookup, crl_path, X509_FILETYPE_PEM);
    }
  z_leave();
  return store;
}

typedef struct _ZSSLCADirectory
{
  time_t modtime;
  STACK_OF(X509_NAME) *contents;
} ZSSLCADirectory;

static int 
z_ssl_X509_name_cmp(X509_NAME **a, X509_NAME **b)
{
  return(X509_NAME_cmp(*a, *b));
}

gboolean
z_ssl_set_trusted_ca_list(SSL_CTX *ctx, gchar *ca_path)
{
  ZSSLCADirectory *ca_dir = NULL;
  static GHashTable *ca_dir_hash = NULL;
  static GStaticMutex lock = G_STATIC_MUTEX_INIT;
  STACK_OF(X509_NAME) *ca_file;
  struct dirent *direntry;
  struct stat ca_stat;
  
  DIR *dir;
 
  z_enter(); 
  g_static_mutex_lock(&lock);

  if (ca_dir_hash == NULL)
    {
      ca_dir_hash = g_hash_table_new(g_str_hash, g_str_equal);
    }
  else
    {
      
      ca_dir = g_hash_table_lookup(ca_dir_hash, ca_path);
      
      if (ca_dir)
        {
          if (stat(ca_path, &ca_stat) >= 0 &&
              ca_dir->modtime == ca_stat.st_mtime)
            {
              SSL_CTX_set_client_CA_list(ctx, ca_dir->contents);
              g_static_mutex_unlock(&lock);
              z_leave();
              return TRUE;
            }
          sk_X509_NAME_free(ca_dir->contents);
          g_free(ca_dir);
        }
    }
  
  if (stat(ca_path, &ca_stat) < 0)
    {
      g_static_mutex_unlock(&lock);
      z_leave();
      return FALSE;
    }
  ca_dir = g_new0(ZSSLCADirectory, 1);
  ca_dir->modtime = ca_stat.st_mtime;
  ca_dir->contents = sk_X509_NAME_new(z_ssl_X509_name_cmp);
  
  dir = opendir(ca_path);
  if (dir)
    {
      while ((direntry = readdir(dir)) != NULL)
        {
          char file_name[256];
          int i;
          
          snprintf(file_name, sizeof(file_name), "%s/%s", ca_path, direntry->d_name);
          ca_file = SSL_load_client_CA_file(file_name);
          
          for (i = 0; ca_file != NULL && i < sk_X509_NAME_num(ca_file); i++)
            {
              if (sk_X509_NAME_find(ca_dir->contents, sk_X509_NAME_value(ca_file, i)) < 0)
                sk_X509_NAME_push(ca_dir->contents, sk_X509_NAME_value(ca_file, i));
            }
        }
    }
  g_hash_table_insert(ca_dir_hash, ca_path, ca_file);
  SSL_CTX_set_client_CA_list(ctx, ca_dir->contents);
  g_static_mutex_unlock(&lock);
  
  z_leave();
  return TRUE;
}


/* FIXME: SSL context cache */
ZSSLSession *
z_ssl_session_new(char *session_id, 
                  int mode,
                  gchar *key_file, 
                  gchar *cert_file, 
                  gchar *ca_dir, 
                  gchar *crl_dir, 
                  int verify_depth,
                  int verify_type)
{
  ZSSLSession *self;
  SSL_CTX *ctx;
  SSL *session;
  X509_STORE *crl_store = NULL;
  int verify_mode = 0;
  int ret;
  char buf[128];
  
  z_enter();
  
  if (mode == Z_SSL_MODE_CLIENT)
    ctx = SSL_CTX_new(SSLv23_client_method());
  else
    ctx = SSL_CTX_new(SSLv23_server_method());
  SSL_CTX_set_options(ctx, SSL_OP_ALL);
  if (!ctx)
    {
      /*LOG
        This message indicates that an SSL_CTX couldn't be allocated, it
        usually means that Zorp has not enough memory. You might want
        to check your ulimit settings and/or the available physical/virtual
        RAM in your firewall host.
       */
      z_log(session_id, CORE_ERROR, 3, "Error allocating new SSL_CTX;");
      z_leave();
      return NULL;
    }
  
  if (key_file && key_file[0])
    {
      ret = SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM);
      if (!ret)
        {
          /*LOG
            This message indicates that the given private key could not be 
            loaded. Either the private key is not in PEM format, or 
            the key data was encrypted.
           */
          z_log(session_id, CORE_ERROR, 3, "Error loading private key; keyfile='%s', error='%s'", key_file, z_ssl_get_error_str(buf, sizeof(buf)));
          SSL_CTX_free(ctx);
          z_leave();
          return NULL;
        }
      ret = SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM);
      if (!ret)
        {
          /*LOG
            This message indicates that the given certificate file could not
            be loaded. It might not be in PEM format or otherwise corrupted.
           */
          z_log(session_id, CORE_ERROR, 3, "Error loading certificate file; keyfile='%s', certfile='%s', error='%s'", key_file, cert_file, z_ssl_get_error_str(buf, sizeof(buf)));
          SSL_CTX_free(ctx);
          z_leave();
          return NULL;        
        }
      ret = SSL_CTX_check_private_key(ctx);
      if (!ret)
        {
          /*LOG
            This message indicates that the private key and corresponding
            certificate do not match.
           */
          z_log(session_id, CORE_ERROR, 3, "Certificate and private key mismatch; keyfile='%s', certfile='%s', error='%s'", key_file, cert_file, z_ssl_get_error_str(buf, sizeof(buf)));
          SSL_CTX_free(ctx);
          z_leave();
          return NULL;
        }
      /*LOG
        This informational message indicates that the given private key- and
        certificate files were successfully loaded.
       */
      z_log(session_id, CORE_DEBUG, 6, "Certificate file successfully loaded; keyfile='%s', certfile='%s'", key_file, cert_file);
    }
  if (ca_dir && ca_dir[0])
    {
      if (mode == Z_SSL_MODE_SERVER)
        {
          if (!z_ssl_set_trusted_ca_list(ctx, ca_dir))
            {
              /*LOG
                This message indicates that loading the trustable CA certificates
                failed. The TLS peer is told which CAs we trust to ease the
                selection of certificates. This could mean that one or more
                of your CA certificates are invalid.
               */
              z_log(session_id, CORE_ERROR, 3, "Error loading trusted CA list; cadir='%s'", ca_dir);
              SSL_CTX_free(ctx);
              z_leave();
              return NULL;
            }
        }
      if (!SSL_CTX_load_verify_locations(ctx, NULL, ca_dir))
        {
          /*LOG
            This message indicates that setting the trusted CA directory for
            verification failed.
           */
          z_log(session_id, CORE_ERROR, 3, "Cannot add trusted CA directory as verify location; cadir='%s'", ca_dir);
          SSL_CTX_free(ctx);
          z_leave();
          return NULL;
        }
      if (crl_dir && crl_dir[0])
        {
          crl_store = z_ssl_crl_store_create(crl_dir);
        }
      else
        {
          /*LOG
            This debug message indicates that CRLs are not in use for certificate
            verification.
           */
          z_log(session_id, CORE_DEBUG, 5, "Certificate Revocation Lists not available;");
        }
    }
  session = SSL_new(ctx);
  SSL_CTX_free(ctx);
  if (!session)
    {
      /*LOG
        This message indicates that creating the SSL session structure failed.
        It's usually caused by out-of-memory conditions.
       */
      z_log(session_id, CORE_ERROR, 3, "Error creating SSL session from SSL_CTX;");
      if (crl_store)
        X509_STORE_free(crl_store);
      z_leave();
      return NULL;
    }
  self = g_new0(ZSSLSession, 1);

  self->ssl = session;  
  self->session_id = session_id;
  self->verify_type = verify_type;
  self->verify_depth = verify_depth;
  self->crl_store = crl_store;
  SSL_set_app_data(session, self);

  if (verify_type == Z_SSL_VERIFY_OPTIONAL || 
      verify_type == Z_SSL_VERIFY_REQUIRED_UNTRUSTED)
    {
      verify_mode = SSL_VERIFY_PEER;
    }
  if (verify_type == Z_SSL_VERIFY_REQUIRED_TRUSTED)
    {
      verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
    }

  SSL_set_verify(session, verify_mode, z_ssl_verify_callback);
  
  return self;
}

void
z_ssl_session_destroy(ZSSLSession *self)
{
  /* free verify_data */
  SSL_free(self->ssl);
  g_free(self);
}

static void
z_stream_shutdown_ssl(ZStream *s, int how)
{
  /* do nothing */
}

static gboolean
z_stream_wait_ssl(ZStream *self, guint cond, gint timeout)
{
  SSL_SESSION_set_timeout(((GIOSSLChannel *) (self->channel))->ssl->ssl->session, timeout / 1000);
  return TRUE;
}

void 
z_stream_ssl(ZStream *self, ZSSLSession *ssl)
{
  g_io_channel_unref(self->channel);
  self->channel = g_io_channel_ssl_new(ssl);
  self->wait = z_stream_wait_ssl;
  self->shutdown = z_stream_shutdown_ssl;
}
