From d44c30373f9fb3269542cdb0a77f6d82f792e77a Mon Sep 17 00:00:00 2001
From: Yann Leboulanger <asterix@lagaule.org>
Date: Sun, 18 Apr 2010 20:43:40 +0200
Subject: [PATCH] [Calamar and me]sasl-external c2s authentication. Fixes #5704

---
 data/gui/accounts_window.ui | 66 +++++++++++++++++++++++++++++++++++--
 src/common/config.py        |  1 +
 src/common/connection.py    |  2 ++
 src/common/xmpp/tls_nb.py   | 40 +++++++++++++++++++---
 src/config.py               | 39 +++++++++++++++++++---
 src/dialogs.py              | 44 +++++++++++++++++++++++++
 6 files changed, 182 insertions(+), 10 deletions(-)

diff --git a/data/gui/accounts_window.ui b/data/gui/accounts_window.ui
index 96de16994d..6062613982 100644
--- a/data/gui/accounts_window.ui
+++ b/data/gui/accounts_window.ui
@@ -4,7 +4,7 @@
   <!-- interface-naming-policy toplevel-contextual -->
   <object class="GtkListStore" id="liststore1">
     <columns>
-      <!-- column-name item text -->
+      <!-- column-name item -->
       <column type="gchararray"/>
     </columns>
     <data>
@@ -203,7 +203,7 @@
                           <object class="GtkTable" id="table1">
                             <property name="visible">True</property>
                             <property name="border_width">6</property>
-                            <property name="n_rows">5</property>
+                            <property name="n_rows">6</property>
                             <property name="n_columns">3</property>
                             <property name="column_spacing">6</property>
                             <property name="row_spacing">6</property>
@@ -445,6 +445,68 @@
                                 <property name="y_options"></property>
                               </packing>
                             </child>
+                            <child>
+                              <object class="GtkExpander" id="expander2">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <child>
+                                  <object class="GtkHBox" id="hbox2">
+                                    <property name="visible">True</property>
+                                    <property name="spacing">6</property>
+                                    <child>
+                                      <object class="GtkLabel" id="label28">
+                                        <property name="visible">True</property>
+                                        <property name="xalign">0</property>
+                                        <property name="label" translatable="yes">_Client Cert File:</property>
+                                        <property name="use_underline">True</property>
+                                        <property name="mnemonic_widget">cert_entry1</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="position">0</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkEntry" id="cert_entry1">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="tooltip_text" translatable="yes">The path to the client certificate and key in PKCS#12 format</property>
+                                        <property name="invisible_char">&#x25CF;</property>
+                                      </object>
+                                      <packing>
+                                        <property name="position">1</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkButton" id="browse_for_client_cert_button">
+                                        <property name="label" translatable="yes">Browse...</property>
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="receives_default">True</property>
+                                        <property name="tooltip_text" translatable="yes">Choose Client Cert</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">False</property>
+                                        <property name="position">2</property>
+                                      </packing>
+                                    </child>
+                                  </object>
+                                </child>
+                                <child type="label">
+                                  <object class="GtkLabel" id="label2">
+                                    <property name="visible">True</property>
+                                    <property name="label" translatable="yes">Client certificate</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="right_attach">3</property>
+                                <property name="top_attach">5</property>
+                                <property name="bottom_attach">6</property>
+                                <property name="y_options">GTK_FILL</property>
+                              </packing>
+                            </child>
                           </object>
                         </child>
                         <child type="tab">
diff --git a/src/common/config.py b/src/common/config.py
index c676d5d638..842613563d 100644
--- a/src/common/config.py
+++ b/src/common/config.py
@@ -289,6 +289,7 @@ class Config:
                     'name': [ opt_str, '', '', True ],
                     'hostname': [ opt_str, '', '', True ],
                     'anonymous_auth': [ opt_bool, False ],
+                    'client_cert': [ opt_str, '', '', True ],
                     'savepass': [ opt_bool, False ],
                     'password': [ opt_str, '' ],
                     'resource': [ opt_str, 'gajim', '', True ],
diff --git a/src/common/connection.py b/src/common/connection.py
index f12f7f6c86..fb9520e623 100644
--- a/src/common/connection.py
+++ b/src/common/connection.py
@@ -701,6 +701,8 @@ class Connection(CommonConnection, ConnectionHandlers):
                     'ping_alive_every_foo_secs')
         else:
             self.pingalives = 0
+        self.client_cert = gajim.config.get_per('accounts', self.name,
+            'client_cert')
 
     def check_jid(self, jid):
         return helpers.parse_jid(jid)
diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py
index 907868dafe..c87a9ae3d4 100644
--- a/src/common/xmpp/tls_nb.py
+++ b/src/common/xmpp/tls_nb.py
@@ -349,10 +349,42 @@ class NonBlockingTLS(PlugIn):
     def _startSSL_pyOpenSSL(self):
         log.debug("_startSSL_pyOpenSSL called")
         tcpsock = self._owner
-        # See http://docs.python.org/dev/library/ssl.html
-        tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
-        tcpsock._sslContext.set_options(OpenSSL.SSL.OP_NO_SSLv2 | \
-            OpenSSL.SSL.OP_NO_TICKET)
+        conn = tcpsock._owner._caller
+        if conn.client_cert and os.path.exists(conn.client_cert):
+            # FIXME make a checkbox for Client Cert / SSLv23 / TLSv1
+            # If we are going to use a client cert/key pair for authentication,
+            # we choose TLSv1 method.
+            tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
+            log.debug('Using client cert and key from %s' % conn.client_cert)
+            try:
+                p12 = OpenSSL.crypto.load_pkcs12(open(client_cert_path).read())
+            except OpenSSL.crypto.Error, exception_obj:
+                log.warning('Unable to load client pkcs12 certificate from '
+                    'file %s: %s ... Is it a valid PKCS12 cert?' % \
+                (conn.client_cert, exception_obj.args))
+            except:
+                log.warning('Unknown error while loading certificate from file '
+                    '%s' % conn.client_cert)
+            else:
+                log.info('PKCS12 Client cert loaded OK')
+                try:
+                    tcpsock._sslContext.use_certificate(p12.get_certificate())
+                    tcpsock._sslContext.use_privatekey(p12.get_privatekey())
+                    log.info('p12 cert and key loaded')
+                except OpenSSL.crypto.Error, exception_obj:
+                    log.warning('Unable to extract client certificate from '
+                        'file %s' % conn.client_cert)
+                except Exception, msg:
+                    log.warning('Unknown error extracting client certificate '
+                        'from file %s: %s' % (conn.client_cert, msg))
+                else:
+                    log.info('client cert and key loaded OK')
+        else:
+            # See http://docs.python.org/dev/library/ssl.html
+            tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+            tcpsock._sslContext.set_options(OpenSSL.SSL.OP_NO_SSLv2 | \
+                OpenSSL.SSL.OP_NO_TICKET)
+
         tcpsock.ssl_errnum = 0
         tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER,
                 self._ssl_verify_callback)
diff --git a/src/config.py b/src/config.py
index 7583f080e8..26f1cfeafb 100644
--- a/src/config.py
+++ b/src/config.py
@@ -1612,13 +1612,15 @@ class AccountsWindow:
             focused_widget = self.window.get_focus()
             focused_widget_name = focused_widget.get_name()
             if focused_widget_name in ('jid_entry1', 'resource_entry1',
-            'custom_port_entry'):
+            'custom_port_entry', 'cert_entry1'):
                 if focused_widget_name == 'jid_entry1':
                     func = self.on_jid_entry1_focus_out_event
                 elif focused_widget_name == 'resource_entry1':
                     func = self.on_resource_entry1_focus_out_event
                 elif focused_widget_name == 'custom_port_entry':
                     func = self.on_custom_port_entry_focus_out_event
+                elif focused_widget_name == 'cert_entry1':
+                    func = self.on_cert_entry1_focus_out_event
                 if func(focused_widget, None):
                     # Error detected in entry, don't change account, re-put cursor on
                     # previous row
@@ -1641,6 +1643,22 @@ class AccountsWindow:
         self.init_account()
         self.update_proxy_list()
 
+    def on_browse_for_client_cert_button_clicked(self, widget, data=None):
+        def on_ok(widget, path_to_clientcert_file):
+            self.dialog.destroy()
+            if not path_to_clientcert_file:
+                return
+            self.xml.get_widget('cert_entry1').set_text(path_to_clientcert_file)
+            gajim.config.set_per('accounts', self.current_account,
+                'client_cert', path_to_clientcert_file)
+
+        def on_cancel(widget):
+            self.dialog.destroy()
+
+        path_to_clientcert_file = self.xml.get_widget('cert_entry1').get_text()
+        self.dialog = dialogs.ClientCertChooserDialog(path_to_clientcert_file,
+            on_ok, on_cancel)
+
     def update_proxy_list(self):
         if self.current_account:
             our_proxy = gajim.config.get_per('accounts', self.current_account,
@@ -1796,10 +1814,14 @@ class AccountsWindow:
         # Account tab
         self.draw_normal_jid()
         self.xml.get_object('resource_entry1').set_text(gajim.config.get_per(
-                'accounts', account, 'resource'))
+            'accounts', account, 'resource'))
+
+        client_cert = gajim.config.get_per('accounts', account, 'client_cert')
+        self.xml.get_widget('cert_entry1').set_text(client_cert)
+
         self.xml.get_object('adjust_priority_with_status_checkbutton1').\
-                set_active(gajim.config.get_per('accounts', account,
-                'adjust_priority_with_status'))
+            set_active(gajim.config.get_per('accounts', account,
+            'adjust_priority_with_status'))
         spinbutton = self.xml.get_object('priority_spinbutton1')
         if gajim.config.get('enable_negative_priority'):
             spinbutton.set_range(-128, 127)
@@ -2074,6 +2096,15 @@ class AccountsWindow:
             gajim.config.set_per('accounts', self.current_account, 'hostname',
                     jid_splited[1])
 
+    def on_cert_entry1_focus_out_event(self, widget, event):
+        if self.ignore_events:
+            return
+        client_cert = widget.get_text()
+        if self.option_changed('client_cert', client_cert):
+            self.need_relogin = True
+        gajim.config.set_per('accounts', self.current_account, 'client_cert',
+            client_cert)
+
     def on_anonymous_checkbutton1_toggled(self, widget):
         if self.ignore_events:
             return
diff --git a/src/dialogs.py b/src/dialogs.py
index 21ec20114a..976510ec7b 100644
--- a/src/dialogs.py
+++ b/src/dialogs.py
@@ -3900,6 +3900,50 @@ class ProgressDialog:
         return True # WM's X button or Escape key should not destroy the window
 
 
+class ClientCertChooserDialog(FileChooserDialog):
+    def __init__(self, path_to_clientcert_file='', on_response_ok=None,
+    on_response_cancel=None):
+        '''
+        optionally accepts path_to_clientcert_file so it has that as selected
+        '''
+        def on_ok(widget, callback):
+            '''
+            check if file exists and call callback
+            '''
+            path_to_clientcert_file = self.get_filename()
+            path_to_clientcert_file = \
+                gtkgui_helpers.decode_filechooser_file_paths(
+                (path_to_clientcert_file,))[0]
+            if os.path.exists(path_to_clientcert_file):
+                callback(widget, path_to_clientcert_file)
+
+        FileChooserDialog.__init__(self,
+            title_text=_('Choose Client Cert #PCKS12'),
+            action=gtk.FILE_CHOOSER_ACTION_OPEN,
+            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+            gtk.STOCK_OPEN, gtk.RESPONSE_OK),
+            current_folder='',
+            default_response=gtk.RESPONSE_OK,
+            on_response_ok=(on_ok, on_response_ok),
+            on_response_cancel=on_response_cancel)
+
+        filter_ = gtk.FileFilter()
+        filter_.set_name(_('All files'))
+        filter_.add_pattern('*')
+        self.add_filter(filter_)
+
+        filter_ = gtk.FileFilter()
+        filter_.set_name(_('PKCS12 Files'))
+        filter_.add_pattern('*.p12')
+        self.add_filter(filter_)
+        self.set_filter(filter_)
+
+        if path_to_clientcert_file:
+            # set_filename accept only absolute path
+            path_to_clientcert_file = os.path.abspath(path_to_clientcert_file)
+            self.set_filename(path_to_clientcert_file)
+
+
 class SoundChooserDialog(FileChooserDialog):
     def __init__(self, path_to_snd_file='', on_response_ok=None,
                     on_response_cancel=None):
-- 
GitLab