From a4d05b7f3f7d5073e57bff9f60188607c95ab3d2 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sun, 25 Mar 2007 20:18:40 +0000 Subject: [PATCH] When checking a feed, check that the key is valid specifically for the feed's domain. Improved the GUI's trust confirmation box: - Use bold headings and indentation instead of boxes. - If only one key is being queried, don't show the notebook tabs and use the singular form in the prompt to avoid confusion. - Only ask the user to trust the key for its particular domain, if 0launch is new enough to support that. - If the key is already trusted in other domains, display them to the user. git-svn-id: file:///home/talex/Backups/sf.net/Subversion/zero-install/trunk/0launch@1626 9f8c893c-44ee-0310-b757-c8ca8341c71e --- tests/testtrust.py | 16 +++-- zeroinstall/0launch-gui/ZeroInstall-GUI.xml | 2 +- zeroinstall/0launch-gui/trust_box.py | 95 +++++++++++++++++++++++------ zeroinstall/injector/gpg.py | 6 +- zeroinstall/injector/iface_cache.py | 10 +-- zeroinstall/injector/trust.py | 39 +++++++----- 6 files changed, 119 insertions(+), 49 deletions(-) diff --git a/tests/testtrust.py b/tests/testtrust.py index 3946626..dfb273b 100755 --- a/tests/testtrust.py +++ b/tests/testtrust.py @@ -2,6 +2,7 @@ from basetest import BaseTest import sys, tempfile, os, shutil import unittest +from sets import Set thomas_fingerprint = "92429807C9853C0744A68B9AAE07828059A53CC1" @@ -34,6 +35,7 @@ class TestTrust(BaseTest): def testAddDomain(self): assert not trust.trust_db.is_trusted("1234", "0install.net") trust.trust_db.trust_key("1234") + self.assertEquals(Set(['*']), trust.trust_db.get_trust_domains("1234")) assert trust.trust_db.is_trusted("1234") assert trust.trust_db.is_trusted("1234", "0install.net") @@ -50,6 +52,9 @@ class TestTrust(BaseTest): assert trust.trust_db.is_trusted("1234", "0install.net") assert trust.trust_db.is_trusted("1234", "gimp.org") assert not trust.trust_db.is_trusted("1234", "rox.sourceforge.net") + + self.assertEquals(Set(), trust.trust_db.get_trust_domains("99877")) + self.assertEquals(Set(['0install.net', 'gimp.org']), trust.trust_db.get_trust_domains("1234")) def testParallel(self): a = trust.TrustDB() @@ -62,12 +67,11 @@ class TestTrust(BaseTest): assert a.is_trusted("2") def testDomain(self): - a = trust.TrustDB() - self.assertEquals("example.com", a.domain_from_url('http://example.com/foo')) - self.assertRaises(SafeException, lambda: a.domain_from_url('/tmp/feed.xml')) - self.assertRaises(SafeException, lambda: a.domain_from_url('http:///foo')) - self.assertRaises(SafeException, lambda: a.domain_from_url('http://*/foo')) - self.assertRaises(SafeException, lambda: a.domain_from_url('')) + self.assertEquals("example.com", trust.domain_from_url('http://example.com/foo')) + self.assertRaises(SafeException, lambda: trust.domain_from_url('/tmp/feed.xml')) + self.assertRaises(SafeException, lambda: trust.domain_from_url('http:///foo')) + self.assertRaises(SafeException, lambda: trust.domain_from_url('http://*/foo')) + self.assertRaises(SafeException, lambda: trust.domain_from_url('')) suite = unittest.makeSuite(TestTrust) diff --git a/zeroinstall/0launch-gui/ZeroInstall-GUI.xml b/zeroinstall/0launch-gui/ZeroInstall-GUI.xml index 648ac22..92fe836 100644 --- a/zeroinstall/0launch-gui/ZeroInstall-GUI.xml +++ b/zeroinstall/0launch-gui/ZeroInstall-GUI.xml @@ -10,6 +10,6 @@ - + diff --git a/zeroinstall/0launch-gui/trust_box.py b/zeroinstall/0launch-gui/trust_box.py index f8ee958..848dc42 100644 --- a/zeroinstall/0launch-gui/trust_box.py +++ b/zeroinstall/0launch-gui/trust_box.py @@ -23,6 +23,12 @@ class TrustBox(dialog.Dialog): def __init__(self, interface, sigs, iface_xml): dialog.Dialog.__init__(self) + if hasattr(trust, 'domain_from_url'): + domain = trust.domain_from_url(interface.uri) + assert domain + else: + domain = None # 0launch <= 0.26 + def destroy(box): global _queue assert _queue[0] is self @@ -30,7 +36,11 @@ class TrustBox(dialog.Dialog): # Remove any queued boxes that are no longer required def still_untrusted(box): for sig in box.valid_sigs: - if trust.trust_db.is_trusted(sig.fingerprint): + if domain is None: + is_trusted = trust.trust_db.is_trusted(sig.fingerprint) + else: + is_trusted = trust.trust_db.is_trusted(sig.fingerprint, domain) + if is_trusted: return False return True if _queue: @@ -58,12 +68,31 @@ class TrustBox(dialog.Dialog): vbox.set_border_width(4) self.vbox.pack_start(vbox, True, True, 0) - label = left('Checking: ' + interface.uri + '\n\n' - 'Please confirm that you trust ' - 'these keys to sign software updates:') - vbox.pack_start(label, False, True, 0) + self.valid_sigs = [s for s in sigs if isinstance(s, gpg.ValidSig)] + if not self.valid_sigs: + raise SafeException('No valid signatures found') notebook = gtk.Notebook() + + if len(self.valid_sigs) == 1: + what = 'this key' + notebook.set_show_tabs(False) + else: + what = 'at least one of these keys' + + if domain: + where = '\nfor the domain "%s"' % domain + else: + where = '' + + message = ('Checking: ' + interface.uri + '\n\n' + + 'Please confirm that you trust %s ' + 'to sign software updates%s:' % (what, where)) + + label = left(message) + label.set_padding(4, 4) + vbox.pack_start(label, False, True, 0) + vbox.pack_start(notebook, True, True, 0) self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) @@ -71,10 +100,6 @@ class TrustBox(dialog.Dialog): self.add_button(gtk.STOCK_ADD, gtk.RESPONSE_OK) self.set_default_response(gtk.RESPONSE_OK) - self.valid_sigs = [s for s in sigs if isinstance(s, gpg.ValidSig)] - if not self.valid_sigs: - raise SafeException('No valid signatures found') - trust_checkbox = {} # Sig -> CheckButton def ok_sensitive(): trust_any = False @@ -96,17 +121,35 @@ class TrustBox(dialog.Dialog): name = None page = gtk.VBox(False, 4) page.set_border_width(8) - page.pack_start(left('Fingerprint: ' + pretty_fp(sig.fingerprint)), False, True, 0) + + def frame(title, content): + frame = gtk.Frame() + label = gtk.Label() + label.set_markup('%s' % title) + frame.set_label_widget(label) + frame.set_shadow_type(gtk.SHADOW_NONE) + if type(content) in (str, unicode): + content = gtk.Label(content) + content.set_alignment(0, 0.5) + frame.add(content) + content.set_padding(8, 4) + page.pack_start(frame, False, True, 0) + + frame('Fingerprint', pretty_fp(sig.fingerprint)) + if name is not None: - page.pack_start(left('Claimed identity: ' + name), False, True, 0) + frame('Claimed identity', name) - frame = gtk.Frame('Unreliable hints database says') - frame.set_border_width(4) hint = left(hints.get(sig.fingerprint, 'Warning: Nothing known about this key!')) hint.set_line_wrap(True) - hint.set_padding(4, 4) - frame.add(hint) - page.pack_start(frame, True, True, 0) + frame('Unreliable hints database says', hint) + #hint.set_padding(4, 4) + + if domain: + already_trusted = trust.trust_db.get_trust_domains(sig.fingerprint) + if already_trusted: + frame('You already trust this key for these domains', + '\n'.join(already_trusted)) trust_checkbox[sig] = gtk.CheckButton('_Trust this key') page.pack_start(trust_checkbox[sig], False, True, 0) @@ -122,14 +165,18 @@ class TrustBox(dialog.Dialog): trust_help.display() return if resp == gtk.RESPONSE_OK: - self.trust_keys([sig for sig in trust_checkbox if trust_checkbox[sig].get_active()]) + self.trust_keys([sig for sig in trust_checkbox if trust_checkbox[sig].get_active()], domain) self.destroy() self.connect('response', response) - def trust_keys(self, sigs): + def trust_keys(self, sigs, domain = None): try: for sig in sigs: - trust.trust_db.trust_key(sig.fingerprint) + if domain is None: + # 0launch <= 0.26 + trust.trust_db.trust_key(sig.fingerprint) + else: + trust.trust_db.trust_key(sig.fingerprint, domain) if hasattr(trust.trust_db, 'notify'): # 0launch >= 0.25 @@ -147,6 +194,16 @@ class TrustBox(dialog.Dialog): _queue = [] def confirm_trust(interface, sigs, iface_xml): + """Display a dialog box asking the user to confirm that one of the + keys is trusted for this domain. If a trust box is already visible, this + one is queued until the existing one is closed. + @param interface: the feed being loaded + @type interface: L{model.Interface} + @param sigs: the signatures on the feed + @type sigs: [L{gpg.Signature}] + @param iface_xml: the downloaded (untrusted) XML document + @type iface_xml: str + """ _queue.append(TrustBox(interface, sigs, iface_xml)) if len(_queue) == 1: _queue[0].show() diff --git a/zeroinstall/injector/gpg.py b/zeroinstall/injector/gpg.py index 10c267c..095fc88 100644 --- a/zeroinstall/injector/gpg.py +++ b/zeroinstall/injector/gpg.py @@ -23,7 +23,7 @@ class Signature: def __init__(self, status): self.status = status - def is_trusted(self): + def is_trusted(self, domain = None): return False def need_key(self): @@ -38,8 +38,8 @@ class ValidSig(Signature): def __str__(self): return "Valid signature from " + self.status[self.FINGERPRINT] - def is_trusted(self): - return trust_db.is_trusted(self.status[self.FINGERPRINT]) + def is_trusted(self, domain = None): + return trust_db.is_trusted(self.status[self.FINGERPRINT], domain) def get_timestamp(self): return int(self.status[self.TIMESTAMP]) diff --git a/zeroinstall/injector/iface_cache.py b/zeroinstall/injector/iface_cache.py index f57da66..04e8414 100644 --- a/zeroinstall/injector/iface_cache.py +++ b/zeroinstall/injector/iface_cache.py @@ -207,7 +207,8 @@ class IfaceCache(object): @rtype: bool @precondition: call L{add_pending} """ - updated = self._oldest_trusted(sigs) + import trust + updated = self._oldest_trusted(sigs, trust.domain_from_url(interface.uri)) if updated is None: return False # None are trusted if interface.uri in self.pending: @@ -418,14 +419,15 @@ class IfaceCache(object): def _get_signature_date(self, uri): """Read the date-stamp from the signature of the cached interface. If the date-stamp is unavailable, returns None.""" + import trust sigs = self.get_cached_signatures(uri) if sigs: - return self._oldest_trusted(sigs) + return self._oldest_trusted(sigs, trust.domain_from_url(uri)) - def _oldest_trusted(self, sigs): + def _oldest_trusted(self, sigs, domain): """Return the date of the oldest trusted signature in the list, or None if there are no trusted sigs in the list.""" - trusted = [s.get_timestamp() for s in sigs if s.is_trusted()] + trusted = [s.get_timestamp() for s in sigs if s.is_trusted(domain)] if trusted: return min(trusted) return None diff --git a/zeroinstall/injector/trust.py b/zeroinstall/injector/trust.py index e566501..50f372d 100644 --- a/zeroinstall/injector/trust.py +++ b/zeroinstall/injector/trust.py @@ -34,6 +34,13 @@ class TrustDB(object): return True # Deprecated return domain in domains or '*' in domains + + def get_trust_domains(self, fingerprint): + """Return the set of domains in which this key is trusted. + If the list includes '*' then the key is trusted everywhere. + """ + self.ensure_uptodate() + return self.keys.get(fingerprint, sets.Set()) def trust_key(self, fingerprint, domain = '*'): """Add key to the list of trusted fingerprints. @@ -115,21 +122,21 @@ class TrustDB(object): if key: self.keys[key] = sets.Set('*') - def domain_from_url(self, url): - """Extract the trust domain for a URL. - @param url: the feed's URL - @type url: str - @return: the trust domain - @rtype: str - @since: 0.27 - @raise SafeException: the URL can't be parsed""" - import urlparse - from zeroinstall import SafeException - if url.startswith('/'): - raise SafeException("Can't get domain from a local path: '%s'" % url) - domain = urlparse.urlparse(url)[1] - if domain and domain != '*': - return domain - raise SafeException("Can't extract domain from URL '%s'" % url) +def domain_from_url(url): + """Extract the trust domain for a URL. + @param url: the feed's URL + @type url: str + @return: the trust domain + @rtype: str + @since: 0.27 + @raise SafeException: the URL can't be parsed""" + import urlparse + from zeroinstall import SafeException + if url.startswith('/'): + raise SafeException("Can't get domain from a local path: '%s'" % url) + domain = urlparse.urlparse(url)[1] + if domain and domain != '*': + return domain + raise SafeException("Can't extract domain from URL '%s'" % url) trust_db = TrustDB() -- 2.11.4.GIT