introducer: add sequence-numbers to announcements, ignore replays

This will support revocation of Accounting recommendation records,
assuming the gossip-based broadcast channel isn't easily jammed.
This commit is contained in:
Brian Warner 2012-06-10 19:10:22 -07:00
parent bf416af49e
commit e1093cbb33
3 changed files with 137 additions and 10 deletions

View File

@ -201,8 +201,9 @@ class IntroducerClient(service.Service, Referenceable):
d.addCallback(_publish_stub_client)
return d
def create_announcement(self, service_name, ann, signing_key):
def create_announcement(self, service_name, ann, signing_key, _mod=None):
full_ann = { "version": 0,
"seqnum": time.time(),
"nickname": self._nickname,
"app-versions": self._app_versions,
"my-version": self._my_version,
@ -211,6 +212,8 @@ class IntroducerClient(service.Service, Referenceable):
"service-name": service_name,
}
full_ann.update(ann)
if _mod:
full_ann = _mod(full_ann) # for unit tests
return sign_to_foolscap(full_ann, signing_key)
def publish(self, service_name, ann, signing_key=None):
@ -303,8 +306,27 @@ class IntroducerClient(service.Service, Referenceable):
parent=lp2, level=log.UNUSUAL, umid="B1MIdA")
self._debug_counts["duplicate_announcement"] += 1
return
# does it update an existing one?
if index in self._current_announcements:
old,_,_ = self._current_announcements[index]
if "seqnum" in old:
# must beat previous sequence number to replace
if "seqnum" not in ann:
self.log("not replacing old announcement, no seqnum: %s"
% (ann,),
parent=lp2, level=log.NOISY, umid="zFGH3Q")
return
if ann["seqnum"] <= old["seqnum"]:
# note that exact replays are caught earlier, by
# comparing the entire signed announcement.
self.log("not replacing old announcement, "
"new seqnum is too old (%s <= %s) "
"(replay attack?): %s"
% (ann["seqnum"], old["seqnum"], ann),
parent=lp2, level=log.UNUSUAL, umid="JAAAoQ")
return
# ok, seqnum is newer, allow replacement
self._debug_counts["update"] += 1
self.log("replacing old announcement: %s" % (ann,),
parent=lp2, level=log.NOISY, umid="wxwgIQ")

View File

@ -120,6 +120,8 @@ class IntroducerService(service.MultiService, Referenceable):
self._debug_counts = {"inbound_message": 0,
"inbound_duplicate": 0,
"inbound_no_seqnum": 0,
"inbound_old_replay": 0,
"inbound_update": 0,
"outbound_message": 0,
"outbound_announcements": 0,
@ -216,6 +218,21 @@ class IntroducerService(service.MultiService, Referenceable):
self._debug_counts["inbound_duplicate"] += 1
return
else:
if "seqnum" in old_ann:
# must beat previous sequence number to replace
if "seqnum" not in ann:
self.log("not replacing old ann, no seqnum",
level=log.NOISY, umid="ySbaVw")
self._debug_counts["inbound_no_seqnum"] += 1
return
if ann["seqnum"] <= old_ann["seqnum"]:
self.log("not replacing old ann, new seqnum is too old"
" (%s <= %s) (replay attack?)"
% (ann["seqnum"], old_ann["seqnum"]),
level=log.UNUSUAL, umid="sX7yqQ")
self._debug_counts["inbound_old_replay"] += 1
return
# ok, seqnum is newer, allow replacement
self.log("old announcement being updated", level=log.NOISY,
umid="304r9g")
self._debug_counts["inbound_update"] += 1

View File

@ -117,8 +117,13 @@ def make_ann(furl):
"permutation-seed-base32": get_tubid_string(furl) }
return ann
def make_ann_t(ic, furl, privkey):
return ic.create_announcement("storage", make_ann(furl), privkey)
def make_ann_t(ic, furl, privkey, seqnum):
def mod(ann):
ann["seqnum"] = seqnum
if seqnum is None:
del ann["seqnum"]
return ann
return ic.create_announcement("storage", make_ann(furl), privkey, mod)
class Client(unittest.TestCase):
def test_duplicate_receive_v1(self):
@ -196,10 +201,12 @@ class Client(unittest.TestCase):
# ann1b: ic2, furl1
# ann2: ic2, furl2
self.ann1 = make_ann_t(ic1, furl1, privkey)
self.ann1a = make_ann_t(ic1, furl1a, privkey)
self.ann1b = make_ann_t(ic2, furl1, privkey)
self.ann2 = make_ann_t(ic2, furl2, privkey)
self.ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
self.ann1old = make_ann_t(ic1, furl1, privkey, seqnum=9)
self.ann1noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
self.ann1b = make_ann_t(ic2, furl1, privkey, seqnum=11)
self.ann1a = make_ann_t(ic1, furl1a, privkey, seqnum=12)
self.ann2 = make_ann_t(ic2, furl2, privkey, seqnum=13)
ic1.remote_announce_v2([self.ann1]) # queues eventual-send
d = fireEventually()
@ -219,6 +226,20 @@ class Client(unittest.TestCase):
self.failUnlessEqual(len(announcements), 1)
d.addCallback(_then2)
# an older announcement shouldn't fire the subscriber either
d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1old]))
d.addCallback(fireEventually)
def _then2a(ign):
self.failUnlessEqual(len(announcements), 1)
d.addCallback(_then2a)
# announcement with no seqnum cannot replace one with-seqnum
d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1noseqnum]))
d.addCallback(fireEventually)
def _then2b(ign):
self.failUnlessEqual(len(announcements), 1)
d.addCallback(_then2b)
# and a replacement announcement: same FURL, new other stuff. The
# subscriber *should* be fired.
d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1b]))
@ -298,6 +319,73 @@ class Client(unittest.TestCase):
d.addCallback(_then2)
return d
class Server(unittest.TestCase):
def test_duplicate(self):
i = IntroducerService()
ic1 = IntroducerClient(None,
"introducer.furl", u"my_nickname",
"ver23", "oldest_version", {})
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp"
privkey_s, _ = keyutil.make_keypair()
privkey, _ = keyutil.parse_privkey(privkey_s)
ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
ann1_old = make_ann_t(ic1, furl1, privkey, seqnum=9)
ann1_new = make_ann_t(ic1, furl1, privkey, seqnum=11)
ann1_noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
i.remote_publish_v2(ann1, None)
all = i.get_announcements()
self.failUnlessEqual(len(all), 1)
self.failUnlessEqual(all[0].announcement["seqnum"], 10)
self.failUnlessEqual(i._debug_counts["inbound_message"], 1)
self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 0)
self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 0)
self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
i.remote_publish_v2(ann1, None)
all = i.get_announcements()
self.failUnlessEqual(len(all), 1)
self.failUnlessEqual(all[0].announcement["seqnum"], 10)
self.failUnlessEqual(i._debug_counts["inbound_message"], 2)
self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 0)
self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
i.remote_publish_v2(ann1_old, None)
all = i.get_announcements()
self.failUnlessEqual(len(all), 1)
self.failUnlessEqual(all[0].announcement["seqnum"], 10)
self.failUnlessEqual(i._debug_counts["inbound_message"], 3)
self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
i.remote_publish_v2(ann1_new, None)
all = i.get_announcements()
self.failUnlessEqual(len(all), 1)
self.failUnlessEqual(all[0].announcement["seqnum"], 11)
self.failUnlessEqual(i._debug_counts["inbound_message"], 4)
self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
i.remote_publish_v2(ann1_noseqnum, None)
all = i.get_announcements()
self.failUnlessEqual(len(all), 1)
self.failUnlessEqual(all[0].announcement["seqnum"], 11)
self.failUnlessEqual(i._debug_counts["inbound_message"], 5)
self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 1)
self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
NICKNAME = u"n\u00EDickname-%s" # LATIN SMALL LETTER I WITH ACUTE
class SystemTestMixin(ServiceMixin, pollmixin.PollMixin):
@ -736,7 +824,7 @@ class ClientInfo(unittest.TestCase):
client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2",
"my_version", "oldest", app_versions)
#furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
#ann_s = make_ann_t(client_v2, furl1, None)
#ann_s = make_ann_t(client_v2, furl1, None, 10)
#introducer.remote_publish_v2(ann_s, Referenceable())
subscriber = FakeRemoteReference()
introducer.remote_subscribe_v2(subscriber, "storage",
@ -798,7 +886,7 @@ class Announcements(unittest.TestCase):
"my_version", "oldest", app_versions)
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
tubid = "62ubehyunnyhzs7r6vdonnm2hpi52w6y"
ann_s0 = make_ann_t(client_v2, furl1, None)
ann_s0 = make_ann_t(client_v2, furl1, None, 10.0)
canary0 = Referenceable()
introducer.remote_publish_v2(ann_s0, canary0)
a = introducer.get_announcements()
@ -821,7 +909,7 @@ class Announcements(unittest.TestCase):
sk_s, vk_s = keyutil.make_keypair()
sk, _ignored = keyutil.parse_privkey(sk_s)
pks = keyutil.remove_prefix(vk_s, "pub-")
ann_t0 = make_ann_t(client_v2, furl1, sk)
ann_t0 = make_ann_t(client_v2, furl1, sk, 10.0)
canary0 = Referenceable()
introducer.remote_publish_v2(ann_t0, canary0)
a = introducer.get_announcements()