From 210daef5cfbbb4aefcd83c9376589c11c8a35f03 Mon Sep 17 00:00:00 2001
From: Maarten de Vries Haha <maarten@de-vri.es>
Date: Mon, 20 Jan 2014 02:11:33 +0100
Subject: [PATCH] irc: add option to accept server certificate with fingerprint

---
 src/plugins/irc/irc-config.c |  13 +++++
 src/plugins/irc/irc-server.c | 115 +++++++++++++++++++++++++++++++++++++++++--
 src/plugins/irc/irc-server.h |   1 +
 3 files changed, 125 insertions(+), 4 deletions(-)

diff --git a/src/plugins/irc/irc-config.c b/src/plugins/irc/irc-config.c
index 36dc8f4..9a026dc 100644
--- a/src/plugins/irc/irc-config.c
+++ b/src/plugins/irc/irc-config.c
@@ -1581,6 +1581,19 @@ irc_config_server_new_option (struct t_config_file *config_file,
                 callback_change, callback_change_data,
                 NULL, NULL);
             break;
+        case IRC_SERVER_OPTION_SSL_FINGERPRINT:
+            new_option = weechat_config_new_option (
+                config_file, section,
+                option_name, "string",
+                N_("only accept the the server certificate if it has this SHA1 fingerprint"
+                   "(the fingerprint must be encoded as a hexadecimal string without seperators between the bytes)"),
+                NULL, 0, 0,
+                default_value, value,
+                null_value_allowed,
+                callback_check_value, callback_check_value_data,
+                callback_change, callback_change_data,
+                NULL, NULL);
+            break;
         case IRC_SERVER_OPTION_PASSWORD:
             new_option = weechat_config_new_option (
                 config_file, section,
diff --git a/src/plugins/irc/irc-server.c b/src/plugins/irc/irc-server.c
index c058ba9..dd655d6 100644
--- a/src/plugins/irc/irc-server.c
+++ b/src/plugins/irc/irc-server.c
@@ -72,7 +72,7 @@ struct t_irc_message *irc_msgq_last_msg = NULL;
 
 char *irc_server_option_string[IRC_SERVER_NUM_OPTIONS] =
 { "addresses", "proxy", "ipv6",
-  "ssl", "ssl_cert", "ssl_priorities", "ssl_dhkey_size", "ssl_verify",
+  "ssl", "ssl_cert", "ssl_priorities", "ssl_dhkey_size", "ssl_verify", "ssl_fingerprint",
   "password", "capabilities",
   "sasl_mechanism", "sasl_username", "sasl_password", "sasl_timeout",
   "autoconnect", "autoreconnect", "autoreconnect_delay",
@@ -87,7 +87,7 @@ char *irc_server_option_string[IRC_SERVER_NUM_OPTIONS] =
 
 char *irc_server_option_default[IRC_SERVER_NUM_OPTIONS] =
 { "", "", "on",
-  "off", "", "NORMAL", "2048", "on",
+  "off", "", "NORMAL", "2048", "on", "",
   "", "",
   "plain", "", "", "15",
   "off", "on", "10",
@@ -3525,6 +3525,84 @@ irc_server_create_buffer (struct t_irc_server *server)
 }
 
 #ifdef HAVE_GNUTLS
+
+/*
+ * Check if a gnutls_session uses the certificate with the provided fingerprint.
+ */
+int
+irc_server_test_certificate_fingerprint (gnutls_session_t session, const struct t_irc_server * server, const char * good_fingerprint)
+{
+    const gnutls_datum_t *chain;
+    unsigned int chain_size;
+    gnutls_x509_crt_t certificate;
+    char fingerprint[20];
+    size_t fingerprint_size;
+    size_t i;
+    int buffer;
+
+    // Get the certificate chain.
+    chain = gnutls_certificate_get_peers (session, &chain_size);
+    if (!chain || !chain_size) {
+        weechat_printf (server->buffer,
+                        _("%sgnutls: no server certificate found"),
+                        weechat_prefix("error"));
+        return 0;
+    }
+
+    // Initalize the certificate structure.
+    if (gnutls_x509_crt_init (&certificate) != GNUTLS_E_SUCCESS)
+    {
+        weechat_printf (server->buffer,
+                        _("%sgnutls: failed to initialize certificate structure"),
+                        weechat_prefix("error"));
+        return 0;
+    }
+
+    // Import the raw certificate data.
+    if (gnutls_x509_crt_import (certificate,
+                               &chain[0],
+                               GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS)
+    {
+        gnutls_x509_crt_deinit (certificate);
+        weechat_printf (server->buffer,
+                        _("%sgnutls: failed to import server certificate"),
+                        weechat_prefix ("error"));
+        return 0;
+    }
+
+    // Calculate the SHA1 fingerprint for the certificate.
+    fingerprint_size = sizeof (fingerprint);
+    if (gnutls_x509_crt_get_fingerprint (certificate,
+                                         GNUTLS_DIG_SHA1,
+                                         fingerprint,
+                                         &fingerprint_size) != GNUTLS_E_SUCCESS)
+    {
+        weechat_printf (server->buffer,
+                        _("%sgnutls: failed to calculate server fingerprint"),
+                        weechat_prefix ("error"));
+        gnutls_x509_crt_deinit (certificate);
+        return 0;
+    }
+
+    // Clean up.
+    gnutls_x509_crt_deinit (certificate);
+
+    // Compare the fingerprints.
+    for (i = 0; i < fingerprint_size; ++i)
+    {
+        // Make sure good_fingerprint doesn't end too early.
+        if (!good_fingerprint[i*2] || !good_fingerprint[i*2+1])
+            return 0;
+        // Parse one byte and make sure it was parsed correctly.
+        if (sscanf (&good_fingerprint[i*2], "%02x", &buffer) != 1)
+            return 0;
+        // Make sure the parsed byte matches the actual fingerprint.
+        if ((char) buffer != fingerprint[i])
+            return 0;
+    }
+    return 1;
+}
+
 /*
  * GnuTLS callback called during handshake.
  *
@@ -3553,7 +3631,7 @@ irc_server_gnutls_callback (void *data, gnutls_session_t tls_session,
     unsigned int cert_list_len, status;
     time_t cert_time;
     char *cert_path0, *cert_path1, *cert_path2, *cert_str, *hostname;
-    const char *weechat_dir;
+    const char *weechat_dir, *fingerprint;
     int rc, ret, i, j, hostname_match;
 #if LIBGNUTLS_VERSION_NUMBER >= 0x010706
     gnutls_datum_t cinfo;
@@ -3583,7 +3661,26 @@ irc_server_gnutls_callback (void *data, gnutls_session_t tls_session,
                         weechat_prefix ("network"),
                         IRC_SERVER_OPTION_INTEGER (server,
                                                    IRC_SERVER_OPTION_SSL_DHKEY_SIZE));
-        if (gnutls_certificate_verify_peers2 (tls_session, &status) < 0)
+
+        // Skip normal checks if ssl_fingerprint is set and just check that.
+        fingerprint = IRC_SERVER_OPTION_STRING (server,
+                                                IRC_SERVER_OPTION_SSL_FINGERPRINT);
+        if (fingerprint && fingerprint[0]) {
+            if (!irc_server_test_certificate_fingerprint (tls_session, server, fingerprint))
+            {
+                rc = -1;
+                weechat_printf (server->buffer,
+                                _("%sgnutls: server fingerprint did NOT match"),
+                                weechat_prefix ("error"));
+            }
+            else
+            {
+                weechat_printf (server->buffer,
+                                _("%sgnutls: server fingerprint matches"),
+                                weechat_prefix ("network"));
+            }
+        }
+        else if (gnutls_certificate_verify_peers2 (tls_session, &status) < 0)
         {
             weechat_printf (server->buffer,
                             _("%sgnutls: error while checking peer's certificate"),
@@ -4748,6 +4845,9 @@ irc_server_add_to_infolist (struct t_infolist *infolist,
     if (!weechat_infolist_new_var_integer (ptr_item, "ssl_verify",
                                            IRC_SERVER_OPTION_BOOLEAN(server, IRC_SERVER_OPTION_SSL_VERIFY)))
         return 0;
+    if (!weechat_infolist_new_var_string (ptr_item, "ssl_fingerprint",
+                                           IRC_SERVER_OPTION_STRING(server, IRC_SERVER_OPTION_SSL_FINGERPRINT)))
+        return 0;
     if (!weechat_infolist_new_var_string (ptr_item, "password",
                                           IRC_SERVER_OPTION_STRING(server, IRC_SERVER_OPTION_PASSWORD)))
         return 0;
@@ -4977,6 +5077,13 @@ irc_server_print_log ()
             weechat_log_printf ("  ssl_verify . . . . . : %s",
                                 weechat_config_boolean (ptr_server->options[IRC_SERVER_OPTION_SSL_VERIFY]) ?
                                 "on" : "off");
+        /* ssl_fingerprint */
+        if (weechat_config_option_is_null (ptr_server->options[IRC_SERVER_OPTION_SSL_FINGERPRINT]))
+            weechat_log_printf ("  ssl_fingerprint . . . : null ('%s')",
+                                IRC_SERVER_OPTION_STRING(ptr_server, IRC_SERVER_OPTION_SSL_FINGERPRINT));
+        else
+            weechat_log_printf ("  ssl_fingerprint . . . : '%s'",
+                                weechat_config_string (ptr_server->options[IRC_SERVER_OPTION_SSL_FINGERPRINT]));
         /* password */
         if (weechat_config_option_is_null (ptr_server->options[IRC_SERVER_OPTION_PASSWORD]))
             weechat_log_printf ("  password . . . . . . : null");
diff --git a/src/plugins/irc/irc-server.h b/src/plugins/irc/irc-server.h
index e359176..9f59c6e 100644
--- a/src/plugins/irc/irc-server.h
+++ b/src/plugins/irc/irc-server.h
@@ -42,6 +42,7 @@ enum t_irc_server_option
     IRC_SERVER_OPTION_SSL_PRIORITIES, /* gnutls priorities                   */
     IRC_SERVER_OPTION_SSL_DHKEY_SIZE, /* Diffie Hellman key size             */
     IRC_SERVER_OPTION_SSL_VERIFY,    /* check if the connection is trusted   */
+    IRC_SERVER_OPTION_SSL_FINGERPRINT, /* server specific sha1 fingerprint   */
     IRC_SERVER_OPTION_PASSWORD,      /* password for server                  */
     IRC_SERVER_OPTION_CAPABILITIES,  /* client capabilities to enable        */
     IRC_SERVER_OPTION_SASL_MECHANISM,/* mechanism for SASL authentication    */
-- 
1.8.5.2

