commit d046f5b6bf9815a37107262168a054d8e793a2d1 Author: David Fifield david@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@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()