commit 6ad1ce1e3d9f1d088fab8fcc55b7ea9712c9cc42
Author: Damian Johnson <atagar(a)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@.
---
.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(a)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))