#include "config.h"

#include <gnutls/gnutls.h>
#include <telepathy-glib/telepathy-glib.h>
#include <telepathy-glib/telepathy-glib-dbus.h>

#include "empathy-tls-verifier.h"
#include "mock-pkcs11.h"
#include "test-helper.h"

#define MOCK_TLS_CERTIFICATE_PATH "/mock/certificate"

/* Forward decl */
GType mock_tls_certificate_get_type (void);

#define MOCK_TLS_CERTIFICATE(obj) \
  (G_TYPE_CHECK_INSTANCE_CAST((obj), mock_tls_certificate_get_type (), \
    MockTLSCertificate))

typedef struct _MockTLSCertificate {
  GObject parent;
  guint state;
  GPtrArray *rejections;
  gchar *cert_type;
  GPtrArray *cert_data;
} MockTLSCertificate;

typedef struct _MockTLSCertificateClass {
  GObjectClass parent;
  TpDBusPropertiesMixinClass dbus_props_class;
} MockTLSCertificateClass;

enum {
  PROP_0,
  PROP_STATE,
  PROP_REJECTIONS,
  PROP_CERTIFICATE_TYPE,
  PROP_CERTIFICATE_CHAIN_DATA
};

static void mock_tls_certificate_iface_init (gpointer, gpointer);

G_DEFINE_TYPE_WITH_CODE(MockTLSCertificate, mock_tls_certificate, G_TYPE_OBJECT,
        G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_AUTHENTICATION_TLS_CERTIFICATE,
                mock_tls_certificate_iface_init)
        G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES,
                tp_dbus_properties_mixin_iface_init)
)

static void
mock_tls_certificate_init (MockTLSCertificate *self)
{
  self->state = TP_TLS_CERTIFICATE_STATE_PENDING;
  self->cert_type = g_strdup ("x509");
  self->cert_data = g_ptr_array_new_with_free_func ((GDestroyNotify)
      g_array_unref);
  self->rejections = g_ptr_array_new ();
}

static void
mock_tls_certificate_get_property (GObject *object,
        guint property_id,
        GValue *value,
        GParamSpec *pspec)
{
  MockTLSCertificate *self = MOCK_TLS_CERTIFICATE (object);

  switch (property_id)
    {
    case PROP_STATE:
      g_value_set_uint (value, self->state);
      break;
    case PROP_REJECTIONS:
      g_value_set_boxed (value, self->rejections);
      break;
    case PROP_CERTIFICATE_TYPE:
      g_value_set_string (value, self->cert_type);
      break;
    case PROP_CERTIFICATE_CHAIN_DATA:
      g_value_set_boxed (value, self->cert_data);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

static void
mock_tls_certificate_finalize (GObject *object)
{
  MockTLSCertificate *self = MOCK_TLS_CERTIFICATE (object);

  tp_clear_boxed (TP_ARRAY_TYPE_TLS_CERTIFICATE_REJECTION_LIST,
      &self->rejections);
  g_free (self->cert_type);
  self->cert_type = NULL;
  g_ptr_array_unref (self->cert_data);
  self->cert_data = NULL;

  G_OBJECT_CLASS (mock_tls_certificate_parent_class)->finalize (object);
}

static void
mock_tls_certificate_class_init (MockTLSCertificateClass *klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);
  GParamSpec *pspec;

  static TpDBusPropertiesMixinPropImpl object_props[] = {
          { "State", "state", NULL },
          { "Rejections", "rejections", NULL },
          { "CertificateType", "certificate-type", NULL },
          { "CertificateChainData", "certificate-chain-data", NULL },
          { NULL }
  };

  static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
    { TP_IFACE_AUTHENTICATION_TLS_CERTIFICATE,
      tp_dbus_properties_mixin_getter_gobject_properties,
      NULL,
      object_props,
    },
    { NULL }
  };

  oclass->get_property = mock_tls_certificate_get_property;
  oclass->finalize = mock_tls_certificate_finalize;

  pspec = g_param_spec_uint ("state",
      "State of this certificate",
      "The state of this TLS certificate.",
      0, TP_NUM_TLS_CERTIFICATE_STATES - 1,
      TP_TLS_CERTIFICATE_STATE_PENDING,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (oclass, PROP_STATE, pspec);

  pspec = g_param_spec_boxed ("rejections",
      "The reject reasons",
      "The reasons why this TLS certificate has been rejected",
      TP_ARRAY_TYPE_TLS_CERTIFICATE_REJECTION_LIST,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (oclass, PROP_REJECTIONS, pspec);

  pspec = g_param_spec_string ("certificate-type",
      "The certificate type",
      "The type of this certificate.",
      NULL,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (oclass, PROP_CERTIFICATE_TYPE, pspec);

  pspec = g_param_spec_boxed ("certificate-chain-data",
      "The certificate chain data",
      "The raw PEM-encoded trust chain of this certificate.",
      TP_ARRAY_TYPE_UCHAR_ARRAY_LIST,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (oclass, PROP_CERTIFICATE_CHAIN_DATA, pspec);

  klass->dbus_props_class.interfaces = prop_interfaces;
  tp_dbus_properties_mixin_class_init (oclass,
      G_STRUCT_OFFSET (MockTLSCertificateClass, dbus_props_class));
}

static void
mock_tls_certificate_accept (TpSvcAuthenticationTLSCertificate *base,
        DBusGMethodInvocation *context)
{
  MockTLSCertificate *self = MOCK_TLS_CERTIFICATE (base);
  self->state = TP_TLS_CERTIFICATE_STATE_ACCEPTED;
  tp_svc_authentication_tls_certificate_emit_accepted (self);
  tp_svc_authentication_tls_certificate_return_from_accept (context);
}

static void
mock_tls_certificate_reject (TpSvcAuthenticationTLSCertificate *base,
        const GPtrArray *in_Rejections,
        DBusGMethodInvocation *context)
{
  MockTLSCertificate *self = MOCK_TLS_CERTIFICATE (base);
  self->state = TP_TLS_CERTIFICATE_STATE_REJECTED;
  tp_svc_authentication_tls_certificate_emit_rejected (self, in_Rejections);
  tp_svc_authentication_tls_certificate_return_from_reject (context);
}

static void
mock_tls_certificate_iface_init (gpointer g_iface,
        gpointer iface_data)
{
  TpSvcAuthenticationTLSCertificateClass *klass =
    (TpSvcAuthenticationTLSCertificateClass *) g_iface;

  tp_svc_authentication_tls_certificate_implement_accept (klass,
      mock_tls_certificate_accept);
  tp_svc_authentication_tls_certificate_implement_reject (klass,
      mock_tls_certificate_reject);
}

#if 0
static void
mock_tls_certificate_assert_rejected (MockTLSCertificate *self,
        TpTLSCertificateRejectReason reason)
{
  GValueArray *rejection;
  TpTLSCertificateRejectReason rejection_reason;
  gchar *rejection_error;
  GHashTable *rejection_details;
  guint i;

  g_assert (self->state == TP_TLS_CERTIFICATE_STATE_REJECTED);
  g_assert (self->rejections);
  g_assert (self->rejections->len > 0);

  for (i = 0; i < self->rejections->len; ++i)
    {
      rejection = g_ptr_array_index (self->rejections, i);
      tp_value_array_unpack (rejection, 3,
              G_TYPE_UINT, &rejection_reason,
              G_TYPE_STRING, &rejection_error,
              TP_HASH_TYPE_STRING_VARIANT_MAP, &rejection_details,
              NULL);
      g_free (rejection_error);
      g_hash_table_unref (rejection_details);

      if (rejection_reason == reason)
        return;
    }

  g_assert ("Certificate was not rejected for right reason" && 0);
}
#endif

static MockTLSCertificate *
mock_tls_certificate_new_and_register (TpDBusDaemon *dbus,
        const gchar *path,
        ...)
{
  MockTLSCertificate *cert;
  GError *error = NULL;
  gchar *filename, *contents;
  GArray *der;
  gsize length;
  va_list va;

  cert = g_object_new (mock_tls_certificate_get_type (), NULL);

  va_start (va, path);
  while (path != NULL) {
      filename = g_build_filename (g_getenv ("EMPATHY_SRCDIR"),
              "tests", "certificates", path, NULL);
      g_file_get_contents (filename, &contents, &length, &error);
      g_assert_no_error (error);

      der = g_array_sized_new (TRUE, TRUE, sizeof (guchar), length);
      g_array_append_vals (der, contents, length);
      g_ptr_array_add (cert->cert_data, der);

      g_free (contents);
      g_free (filename);

      path = va_arg (va, gchar*);
  }
  va_end (va);

  tp_dbus_daemon_register_object (dbus, MOCK_TLS_CERTIFICATE_PATH, cert);
  return cert;
}

/* ----------------------------------------------------------------------------
 * TESTS
 */

typedef struct {
  GMainLoop *loop;
  TpDBusDaemon *dbus;
  const gchar *dbus_name;
  MockTLSCertificate *mock;
  TpTLSCertificate *cert;
  GAsyncResult *result;
} Test;

static void
setup (Test *test, gconstpointer data)
{
  GError *error = NULL;
  GckModule *module;
  const gchar *trust_uris[2] = { MOCK_SLOT_ONE_URI, NULL };

  test->loop = g_main_loop_new (NULL, FALSE);

  test->dbus = tp_dbus_daemon_dup (&error);
  g_assert_no_error (error);

  test->dbus_name = tp_dbus_daemon_get_unique_name (test->dbus);

  test->result = NULL;
  test->cert = NULL;

  /* Add our mock module as the only PKCS#11 module */
  module = gck_module_new (&mock_default_functions);
  mock_C_Initialize (NULL);

  gcr_pkcs11_set_modules (NULL);
  gcr_pkcs11_add_module (module);
  gcr_pkcs11_set_trust_lookup_uris (trust_uris);
}

static void
teardown (Test *test, gconstpointer data)
{
  mock_C_Finalize (NULL);

  test->dbus_name = NULL;

  if (test->mock)
    {
      tp_dbus_daemon_unregister_object (test->dbus, test->mock);
      g_object_unref (test->mock);
      test->mock = NULL;
    }

  if (test->result)
    g_object_unref (test->result);
  test->result = NULL;

  if (test->cert)
    g_object_unref (test->cert);
  test->cert = NULL;

  g_main_loop_unref (test->loop);
  test->loop = NULL;

  g_object_unref (test->dbus);
  test->dbus = NULL;
}

static void
add_certificate_to_mock (Test *test,
        const gchar *certificate,
        const gchar *peer)
{
  GError *error = NULL;
  GcrCertificate *cert;
  gchar *contents;
  gsize length;
  gchar *path;

  path = g_build_filename (g_getenv ("EMPATHY_SRCDIR"),
                           "tests", "certificates", certificate, NULL);

  g_file_get_contents (path, &contents, &length, &error);
  g_assert_no_error (error);

  cert = gcr_simple_certificate_new ((const guchar *)contents, length);
  mock_module_add_certificate (cert);
  mock_module_add_assertion (cert,
          peer ? CKT_X_PINNED_CERTIFICATE : CKT_X_ANCHORED_CERTIFICATE,
          GCR_PURPOSE_SERVER_AUTH, peer);
  g_object_unref (cert);

  g_free (contents);
  g_free (path);
}

static void
fetch_callback_result (GObject *object,
        GAsyncResult *res,
        gpointer user_data)
{
  Test *test = user_data;
  g_assert (!test->result);
  test->result = g_object_ref (res);
  g_main_loop_quit (test->loop);
}

static void
ensure_certificate_proxy (Test *test)
{
  GError *error = NULL;
  GQuark features[] = { TP_TLS_CERTIFICATE_FEATURE_CORE, 0 };

  if (test->cert)
    return;

  /* Create and prepare a certificate */
  /* We don't use tp_tls_certificate_new() as we don't pass a parent */
  test->cert = g_object_new (TP_TYPE_TLS_CERTIFICATE,
      "dbus-daemon", test->dbus,
      "bus-name", test->dbus_name,
      "object-path", MOCK_TLS_CERTIFICATE_PATH,
      NULL);

  tp_proxy_prepare_async (test->cert, features, fetch_callback_result, test);
  g_main_loop_run (test->loop);
  tp_proxy_prepare_finish (test->cert, test->result, &error);
  g_assert_no_error (error);

  /* Clear for any future async stuff */
  g_object_unref (test->result);
  test->result = NULL;
}

/* A simple test to make sure the test infrastructure is working */
static void
test_certificate_mock_basics (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  GError *error = NULL;

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", NULL);

  ensure_certificate_proxy (test);

  tp_tls_certificate_accept_async (test->cert, fetch_callback_result, test);
  g_main_loop_run (test->loop);
  tp_tls_certificate_accept_finish (test->cert, test->result, &error);
  g_assert_no_error (error);

  g_assert (test->mock->state == TP_TLS_CERTIFICATE_STATE_ACCEPTED);
}

static void
test_certificate_verify_success_with_pkcs11_lookup (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "test-server.empathy.gnome.org",
    NULL
  };

  /*
   * In this test the mock TLS connection only has one certificate
   * not a full certificat echain. The root anchor certificate is
   * retrieved from PKCS#11 storage.
   */

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", NULL);

  /* We add the collabora directory with the collabora root */
  add_certificate_to_mock (test, "certificate-authority.cer", NULL);

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "test-server.empathy.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);

  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);
  g_assert_no_error (error);

  /* Yay the verification was a success! */

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_success_with_full_chain (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "test-server.empathy.gnome.org",
    NULL
  };

  /*
   * In this test the mock TLS connection has a full certificate
   * chain. We look for an anchor certificate in the chain.
   */

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", "certificate-authority.cer", NULL);

  /* We add the collabora directory with the collabora root */
  add_certificate_to_mock (test, "certificate-authority.cer", NULL);

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "test-server.empathy.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);
  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);
  g_assert_no_error (error);

  /* Yay the verification was a success! */

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_root_not_found (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "test-server.empathy.gnome.org",
    NULL
  };

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", NULL);

  /* Note that we're not adding any place to find root certs */

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "test-server.empathy.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);

  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);

  /* And it should say we're self-signed (oddly enough) */
  g_assert_error (error, G_IO_ERROR,
      TP_TLS_CERTIFICATE_REJECT_REASON_SELF_SIGNED);

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_root_not_anchored (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "test-server.empathy.gnome.org",
    NULL
  };

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", "certificate-authority.cer", NULL);

  /* Note that we're not adding any place to find root certs */

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "test-server.empathy.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);

  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);

  /* And it should say we're self-signed (oddly enough) */
  g_assert_error (error, G_IO_ERROR,
      TP_TLS_CERTIFICATE_REJECT_REASON_SELF_SIGNED);

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_identities_invalid (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "invalid.host.name",
    NULL
  };

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", "certificate-authority.cer", NULL);

  /* We add the collabora directory with the collabora root */
  add_certificate_to_mock (test, "certificate-authority.cer", NULL);

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "invalid.host.name",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);

  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);

  /* And it should say we're self-signed (oddly enough) */
  g_assert_error (error, G_IO_ERROR,
      TP_TLS_CERTIFICATE_REJECT_REASON_HOSTNAME_MISMATCH);

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_uses_reference_identities (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "invalid.host.name",
    NULL
  };

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", "certificate-authority.cer", NULL);

  /* We add the collabora directory with the collabora root */
  add_certificate_to_mock (test, "certificate-authority.cer", NULL);

  ensure_certificate_proxy (test);

  /* Should be using the reference_identities and not host name for checks */
  verifier = empathy_tls_verifier_new (test->cert, "test-server.empathy.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);

  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);

  /* And it should say we're self-signed (oddly enough) */
  g_assert_error (error, G_IO_ERROR,
      TP_TLS_CERTIFICATE_REJECT_REASON_HOSTNAME_MISMATCH);

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_success_with_pinned (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "test-server.empathy.gnome.org",
    NULL
  };

  /*
   * In this test the mock TLS connection has a full certificate
   * chain. We look for an anchor certificate in the chain.
   */

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", NULL);

  /* We add the collabora directory with the collabora root */
  add_certificate_to_mock (test, "server-cert.cer", "test-server.empathy.gnome.org");

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "test-server.empathy.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);
  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);
  g_assert_no_error (error);

  /* Yay the verification was a success! */

  g_clear_error (&error);
  g_object_unref (verifier);
}

static void
test_certificate_verify_pinned_wrong_host (Test *test,
        gconstpointer data G_GNUC_UNUSED)
{
  TpTLSCertificateRejectReason reason = 0;
  GError *error = NULL;
  EmpathyTLSVerifier *verifier;
  const gchar *reference_identities[] = {
    "test-server.empathy.gnome.org",
    NULL
  };

  test->mock = mock_tls_certificate_new_and_register (test->dbus,
          "server-cert.cer", NULL);

  /* Note that we're not adding any place to find root certs */

  ensure_certificate_proxy (test);

  verifier = empathy_tls_verifier_new (test->cert, "another.gnome.org",
      reference_identities);
  empathy_tls_verifier_verify_async (verifier, fetch_callback_result, test);
  g_main_loop_run (test->loop);

  empathy_tls_verifier_verify_finish (verifier, test->result, &reason,
      NULL, &error);

  /* And it should say we're self-signed */
  g_assert_error (error, G_IO_ERROR,
      TP_TLS_CERTIFICATE_REJECT_REASON_SELF_SIGNED);

  g_clear_error (&error);
  g_object_unref (verifier);
}

int
main (int argc,
    char **argv)
{
  int result;

  test_init (argc, argv);
  gnutls_global_init ();

  g_test_add ("/tls/certificate_basics", Test, NULL,
          setup, test_certificate_mock_basics, teardown);
  g_test_add ("/tls/certificate_verify_success_with_pkcs11_lookup", Test, NULL,
          setup, test_certificate_verify_success_with_pkcs11_lookup, teardown);
  g_test_add ("/tls/certificate_verify_success_with_full_chain", Test, NULL,
          setup, test_certificate_verify_success_with_full_chain, teardown);
  g_test_add ("/tls/certificate_verify_root_not_found", Test, NULL,
          setup, test_certificate_verify_root_not_found, teardown);
  g_test_add ("/tls/certificate_verify_root_not_anchored", Test, NULL,
          setup, test_certificate_verify_root_not_anchored, teardown);
  g_test_add ("/tls/certificate_verify_identities_invalid", Test, NULL,
          setup, test_certificate_verify_identities_invalid, teardown);
  g_test_add ("/tls/certificate_verify_uses_reference_identities", Test, NULL,
          setup, test_certificate_verify_uses_reference_identities, teardown);
  g_test_add ("/tls/certificate_verify_success_with_pinned", Test, NULL,
          setup, test_certificate_verify_success_with_pinned, teardown);
  g_test_add ("/tls/certificate_verify_pinned_wrong_host", Test, NULL,
          setup, test_certificate_verify_pinned_wrong_host, teardown);

  result = g_test_run ();
  test_deinit ();
  return result;
}
