[tor-commits] [doctor/master] Script to check for fingerprint changes

atagar at torproject.org atagar at torproject.org
Sun Oct 4 20:25:08 UTC 2015


commit 6ad1ce1e3d9f1d088fab8fcc55b7ea9712c9cc42
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Oct 4 13:25:29 2015 -0700

    Script to check for fingerprint changes
    
    During our recent dev meeting David Goulet asked me for a DocTor check for
    relays rapidly changing their fingerprint. Doing so likely means they're trying
    to get into a privilaged position to snoop on hidden services.
    
    Plan is to keep an eye on this script for a bit (just whipped it up, no doubt
    it has issues), then send the results to David when we have some confidence in
    it. Later the results will be sent to bad-relays at .
---
 .gitignore                    |    1 +
 consensus_health_checker.py   |    2 +-
 descriptor_checker.py         |    2 +-
 fingerprint_change_checker.py |  128 +++++++++++++++++++++++++++++++++++++++++
 sybil_checker.py              |    6 +-
 5 files changed, 134 insertions(+), 5 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1a47934..c320e41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 logs/
 data/contact_information.cfg
 data/fingerprints
+data/fingerprint_changes
 data/last_notified.cfg
 stem
 gmail_pw
diff --git a/consensus_health_checker.py b/consensus_health_checker.py
index 8733cb4..82c21c7 100755
--- a/consensus_health_checker.py
+++ b/consensus_health_checker.py
@@ -742,7 +742,7 @@ def _get_documents(label, resource):
   for authority, query in queries.items():
     try:
       documents[authority] = query.run()[0]
-    except Exception, exc:
+    except Exception as exc:
       if label == 'vote':
         # try to download the vote via the other authorities
 
diff --git a/descriptor_checker.py b/descriptor_checker.py
index 8050ca8..434b4d6 100755
--- a/descriptor_checker.py
+++ b/descriptor_checker.py
@@ -95,7 +95,7 @@ def send_email(subject, descriptor_type, query):
   try:
     timestamp = datetime.datetime.now().strftime("%m/%d/%Y %H:%M")
     util.send(subject, body = EMAIL_BODY % (descriptor_type, query.download_url, timestamp, query.error), to = [util.ERROR_ADDRESS])
-  except Exception, exc:
+  except Exception as exc:
     log.warn("Unable to send email: %s" % exc)
 
 
diff --git a/fingerprint_change_checker.py b/fingerprint_change_checker.py
new file mode 100755
index 0000000..289f3c0
--- /dev/null
+++ b/fingerprint_change_checker.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# Copyright 2015, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Simple script that checks to see if relays rapidly change their finterprint.
+This can indicate malicious intent toward hidden services.
+"""
+
+import datetime
+import time
+import traceback
+
+import util
+
+from stem.descriptor.remote import DescriptorDownloader
+from stem.util import datetime_to_unix, conf
+
+EMAIL_SUBJECT = 'Relays Changing Fingerprint'
+
+EMAIL_BODY = """\
+The following relays are frequently changing their fingerprints...
+
+"""
+
+FINGERPRINT_CHANGES_FILE = util.get_path('data', 'fingerprint_changes')
+THIRTY_DAYS = 30 * 24 * 60 * 60
+
+log = util.get_logger('fingerprint_change_checker')
+
+
+def main():
+  fingerprint_changes = load_fingerprint_changes()
+  downloader = DescriptorDownloader(timeout = 60)
+  alarm_for = []
+
+  for relay in downloader.get_consensus():
+    prior_fingerprints = fingerprint_changes.setdefault((relay.address, relay.or_port), {})
+
+    if relay.fingerprint not in prior_fingerprints:
+      log.debug("Registering a new fingerprint for %s:%s (%s)" % (relay.address, relay.or_port, relay.fingerprint))
+      prior_fingerprints[relay.fingerprint] = datetime_to_unix(relay.published)
+
+      # drop fingerprint changes that are over thirty days old
+
+      for fp in prior_fingerprints:
+        if time.time() - prior_fingerprints[fp] > THIRTY_DAYS:
+          log.debug("Removing fingerprint for %s:%s (%s) which was published %i days ago" % (relay.address, relay.or_port, fp, prior_fingerprints[fp] / 60 / 60 / 24))
+          del prior_fingerprints[fp]
+
+      # if we've changed more than three times in the last thirty days then alarm
+
+      if len(prior_fingerprints) >= 3:
+        alarm_for.append((relay.address, relay.or_port))
+
+  if alarm_for:
+    log.debug("Sending a notification for %i relays..." % len(alarm_for))
+    body = EMAIL_BODY
+
+    for address, or_port in alarm_for:
+      fp_changes = fingerprint_changes[(address, or_port)]
+      log.debug("* %s:%s has had %i fingerprints: %s" % (address, or_port, len(fp_changes), ', '.join(fp_changes.keys())))
+      body += "* %s:%s\n" % (address, or_port)
+
+      for fingerprint, published in fp_changes.items():
+        body += "  %s at %s\n" % (fingerprint, datetime.datetime.fromtimestamp(published).strftime('%Y-%m-%d %H:%M:%S'))
+
+      body += "\n"
+
+    try:
+      util.send(EMAIL_SUBJECT, body = body, to = ['atagar at torproject.org'])
+    except Exception as exc:
+      log.warn("Unable to send email: %s" % exc)
+
+  save_fingerprint_changes(fingerprint_changes)
+
+
+def load_fingerprint_changes():
+  """
+  Loads information about prior fingerprint changes we've persisted. This
+  provides a dictionary of the form...
+
+    (address, or_port) => {fingerprint: published_timestamp...}
+  """
+
+  log.debug("Loading fingerprint changes...")
+  config = conf.get_config('fingerprint_changes')
+
+  try:
+    config.load(FINGERPRINT_CHANGES_FILE)
+    fingerprint_changes = {}
+
+    for key in config.keys():
+      address, or_port = key.split(':', 1)
+
+      for value in config.get(key, []):
+        fingerprint, published = value.split(':', 1)
+        fingerprint_changes.setdefault((address, int(or_port)), {})[fingerprint] = float(published)
+
+    log.debug("  information for %i relays found" % len(fingerprint_changes))
+    return fingerprint_changes
+  except IOError as exc:
+    log.debug("  unable to read '%s': %s" % (FINGERPRINT_CHANGES_FILE, exc))
+    return {}
+
+
+def save_fingerprint_changes(fingerprint_changes):
+  log.debug("Saving fingerprint changes for %i relays" % len(fingerprint_changes))
+  config = conf.get_config('fingerprint_changes')
+  config.clear()
+
+  for address, or_port in fingerprint_changes:
+    for fingerprint, published in fingerprint_changes[(address, or_port)].items():
+      config.set('%s:%s' % (address, or_port), '%s:%s' % (fingerprint, published), overwrite = False)
+
+  try:
+    config.save(FINGERPRINT_CHANGES_FILE)
+  except IOError as exc:
+    log.debug("  unable to save '%s': %s" % (FINGERPRINT_CHANGES_FILE, exc))
+
+
+if __name__ == '__main__':
+  try:
+    main()
+  except:
+    msg = "fingerprint_change_checker.py failed with:\n\n%s" % traceback.format_exc()
+    log.error(msg)
+    util.send("Script Error", body = msg, to = [util.ERROR_ADDRESS])
diff --git a/sybil_checker.py b/sybil_checker.py
index c51d753..52fba0c 100755
--- a/sybil_checker.py
+++ b/sybil_checker.py
@@ -94,7 +94,7 @@ def send_email(new_relays):
     body += "\n".join(relay_entries)
 
     util.send(EMAIL_SUBJECT, body = body)
-  except Exception, exc:
+  except Exception as exc:
     log.warn("Unable to send email: %s" % exc)
 
 
@@ -117,7 +117,7 @@ def load_fingerprints():
 
       log.debug("  %i fingerprints found" % len(fingerprints))
       return set(fingerprints)
-  except Exception, exc:
+  except Exception as exc:
     log.debug("  unable to read '%s': %s" % (FINGERPRINTS_FILE, exc))
     return set()
 
@@ -131,7 +131,7 @@ def save_fingerprints(fingerprints):
 
     with open(FINGERPRINTS_FILE, 'w') as fingerprint_file:
       fingerprint_file.write('\n'.join(fingerprints))
-  except Exception, exc:
+  except Exception as exc:
     log.debug("Unable to save fingerprints to '%s': %s" % (FINGERPRINTS_FILE, exc))
 
 



More information about the tor-commits mailing list