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">●</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