[tor-commits] [flashproxy/master] Add facilitator IMAP Gmail poller.

dcf at torproject.org dcf at torproject.org
Fri Sep 28 06:11:19 UTC 2012


commit d046f5b6bf9815a37107262168a054d8e793a2d1
Author: David Fifield <david at bamsoftware.com>
Date:   Wed Sep 19 13:09:49 2012 -0700

    Add facilitator IMAP Gmail poller.
---
 facilitator/facilitator-email-poller |  234 ++++++++++++++++++++++++++++++++++
 1 files changed, 234 insertions(+), 0 deletions(-)

diff --git a/facilitator/facilitator-email-poller b/facilitator/facilitator-email-poller
new file mode 100755
index 0000000..c9d9a84
--- /dev/null
+++ b/facilitator/facilitator-email-poller
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+
+import email
+import getopt
+import imaplib
+import os
+import socket
+import ssl
+import stat
+import sys
+import tempfile
+import time
+
+import fac
+
+from hashlib import sha1
+from M2Crypto import BIO, RSA, X509
+
+DEFAULT_IMAP_HOST = "imap.gmail.com"
+DEFAULT_IMAP_PORT = 993
+DEFAULT_EMAIL_ADDRESS = "hoddwee at gmail.com"
+
+POLL_INTERVAL = 60
+
+FACILITATOR_ADDR = ("127.0.0.1", 9002)
+
+# We trust no other CA certificate than this.
+#
+# To find the certificate to copy here,
+# $ strace openssl s_client -connect imap.gmail.com:993 -verify 10 -CApath /etc/ssl/certs 2>&1 | grep /etc/ssl/certs
+# stat("/etc/ssl/certs/XXXXXXXX.0", {st_mode=S_IFREG|0644, st_size=YYYY, ...}) = 0
+CA_CERTS = """\
+subject=/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
+issuer=/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
+MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
+dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
+BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
+cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
+AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
+MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
+aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
+IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
+A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
+7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
+1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
+-----END CERTIFICATE-----
+"""
+# SHA-1 digest of expected public key. See
+# http://www.imperialviolet.org/2011/05/04/pinning.html for the reason behind
+# hashing the public key, not the entire certificate.
+PUBKEY_SHA1 = "5d97e1ec007e48c1f36e736e652eeaf2184697c3".decode("hex")
+
+class options(object):
+    email_addr = None
+    imap_addr = None
+    key_filename = None
+    password_filename = None
+    debug = False
+
+def usage(f = sys.stdout):
+    print >> f, """\
+Usage: %(progname)s --key=KEYFILE --pass=PASSFILE
+Facilitator-side helper for the facilitator-reg-email rendezvous. Polls
+an IMAP server for email messages with client registrations, deletes
+them, and forwards the registrations to the facilitator.
+
+  -d, --debug             enable debugging output (Python imaplib messages);
+                            beware that the login password will be shown.
+  -e, --email=ADDRESS     log in as ADDRESS (default "%(email_addr)s").
+  -h, --help              show this help.
+  -i, --imap=HOST[:PORT]  use the given IMAP server (default "%(imap_addr)s").
+  -k, --key=KEYFILE       read a facilitator private key from KEYFILE.
+  -p, --pass=PASSFILE     use the email password contained in PASSFILE.\
+""" % {
+    "progname": sys.argv[0],
+    "email_addr": DEFAULT_EMAIL_ADDRESS,
+    "imap_addr": fac.format_addr((DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)),
+}
+
+options.email_addr = DEFAULT_EMAIL_ADDRESS
+options.imap_addr = (DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "de:hi:k:p:", ["debug", "email=", "help", "imap=", "key=", "pass="])
+for o, a in opts:
+    if o == "-d" or o == "--debug":
+        options.debug = True
+    elif o == "-e" or o == "--email":
+        options.email_addr = a
+    elif o == "-h" or o == "--help":
+        usage()
+        sys.exit()
+    elif o == "-i" or o == "--imap":
+        options.imap_addr = fac.parse_addr_spec(a, DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)
+    elif o == "-k" or o == "--key":
+        options.key_filename = a
+    elif o == "-p" or o == "--pass":
+        options.password_filename = a
+
+if len(args) != 0:
+    usage(sys.stderr)
+    sys.exit(1)
+
+# Return true iff the given fd is readable, writable, and executable only by its
+# owner.
+def check_perms(fd):
+    mode = os.fstat(fd)[0]
+    return (mode & (stat.S_IRWXG | stat.S_IRWXO)) == 0
+
+# Load the email password.
+if options.password_filename is None:
+    print >> sys.stderr, "The --pass option is required."
+    sys.exit(1)
+try:
+    password_file = open(options.password_filename)
+except OSError, e:
+    print >> sys.stderr, """\
+Failed to open password file "%s": %s.\
+""" % (options.password_filename, str(e))
+    sys.exit(1)
+else:
+    if not check_perms(password_file.fileno()):
+        print >> sys.stderr, "Refusing to run with group- or world-readable password file. Try"
+        print >> sys.stderr, "\tchmod 600 %s" % options.password_filename
+        sys.exit(1)
+    email_password = password_file.read().strip()
+finally:
+    password_file.close()
+
+# Load the private key specific to this registration method.
+if options.key_filename is None:
+    print >> sys.stderr, "The --key option is required."
+    sys.exit(1)
+try:
+    key_file = open(options.key_filename)
+except OSError, e:
+    print >> sys.stderr, """\
+Failed to open private key file "%s": %s.\
+""" % (options.key_filename, str(e))
+    sys.exit(1)
+else:
+    if not check_perms(key_file.fileno()):
+        print >> sys.stderr, "Refusing to run with group- or world-readable private key file. Try"
+        print >> sys.stderr, "\tchmod 600 %s" % options.key_filename
+        sys.exit(1)
+    rsa = RSA.load_key_string(key_file.read())
+finally:
+    key_file.close()
+
+class IMAP4_SSL_REQUIRED(imaplib.IMAP4_SSL):
+    """A subclass of of IMAP4_SSL that uses ssl_version=ssl.PROTOCOL_TLSv1 and
+    cert_reqs=ssl.CERT_REQUIRED."""
+    def open(self, host = "", port = imaplib.IMAP4_SSL_PORT):
+        self.host = host
+        self.port = port
+        self.sock = socket.create_connection((host, port))
+        self.sslobj = ssl.wrap_socket(self.sock, ssl_version=ssl.PROTOCOL_TLSv1,
+            cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.certfile)
+        self.file = self.sslobj.makefile('rb')
+
+def find_client_addr(body):
+    """Find and parse the first client line of the form
+        client=...
+    Returns None if no client line was found.
+    """
+    for line in body.splitlines():
+        if line.startswith("client="):
+            _, client_spec = line.split("=", 1)
+            return fac.parse_addr_spec(client_spec)
+    return None
+
+def handle_message(msg):
+    ciphertext = msg.get_payload().decode("base64")
+    plaintext = rsa.private_decrypt(ciphertext, RSA.pkcs1_oaep_padding)
+    client_addr = find_client_addr(plaintext)
+    fac.put_reg(FACILITATOR_ADDR, client_addr)
+
+def imap_loop(imap):
+    while True:
+        typ, data = imap.select()
+        exists = int(data[0])
+
+        for i in range(exists):
+            # Grab and delete message 1 on each iteration; remaining messages
+            # shift down so the next message we process is also message 1.
+            typ, data = imap.fetch(1, "(BODY[])")
+            if data[0] is None:
+                break
+
+            try:
+                msg = email.message_from_string(data[0][1])
+                handle_message(msg)
+            except Exception, e:
+                print >> sys.stderr, "Error processing message, deleting anyway:", e
+
+            imap.store(1, "+FLAGS", "\\Deleted")
+            imap.expunge()
+
+        time.sleep(POLL_INTERVAL)
+
+if options.debug:
+    imaplib.Debug = 4
+
+ca_certs_file = tempfile.NamedTemporaryFile(prefix="facilitator-email-poller-", suffix=".crt", delete=True)
+try:
+    ca_certs_file.write(CA_CERTS)
+    ca_certs_file.flush()
+    imap = IMAP4_SSL_REQUIRED(options.imap_addr[0], options.imap_addr[1],
+        None, ca_certs_file.name)
+finally:
+    ca_certs_file.close()
+
+# Check that the public key is what we expect.
+cert_der = imap.ssl().getpeercert(binary_form=True)
+cert = X509.load_cert_string(cert_der, format=X509.FORMAT_DER)
+pubkey_der = cert.get_pubkey().as_der()
+pubkey_digest = sha1(pubkey_der).digest()
+
+if pubkey_digest != PUBKEY_SHA1:
+    raise ValueError("Public key does not match pin: got %s but expected %s" %
+        (pubkey_digest.encode("hex"), PUBKEY_SHA1.encode("hex")))
+
+imap.login(options.email_addr, email_password)
+
+imap_loop(imap)
+
+imap.close()
+imap.logout()





More information about the tor-commits mailing list