diff --git a/check-starttls.py b/CheckSTARTTLS.py similarity index 100% rename from check-starttls.py rename to CheckSTARTTLS.py diff --git a/config-parser.py b/ConfigParser.py similarity index 97% rename from config-parser.py rename to ConfigParser.py index dbc244b25..8bc7e708f 100755 --- a/config-parser.py +++ b/ConfigParser.py @@ -55,7 +55,7 @@ class Config: self.tls_policies[domain] = {} # being here enforces TLS at all for policy, value in policies.items(): if policy == "min-tls-version": - reasonable = ["TLS", "TLSv1.1", "TLSv1.2", "TLSv1.3"] + reasonable = ["TLS", "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"] if not value in reasonable: raise ValueError, "Not a valid TLS version string: " + `value` self.tls_policies[domain]["min-tls-version"] = str(value) diff --git a/MTAConfigGenerator.py b/MTAConfigGenerator.py new file mode 100755 index 000000000..3b69ba37a --- /dev/null +++ b/MTAConfigGenerator.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python + +import string +import os.path + +POSTFIX_DIR = "/etc/postfix" +DEFAULT_POLICY_FILE = os.path.join(POSTFIX_DIR, "starttls_everywhere_policy") +POLICY_CF_ENTRY="texthash:" + DEFAULT_POLICY_FILE + +def parse_line(line_data): + "return the and right hand sides of stripped, non-comment postfix config line" + # lines are like: + # smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache + num,line = line_data + left, sep, right = line.partition("=") + if not sep: + return None + return (num, left.strip(), right.strip()) + +#def get_cf_values(lines, var): + +class MTAConfigGenerator: + def __init__(self, policy_config): + self.policy_config = policy_config + +class ExistingConfigError(ValueError): pass + +class PostfixConfigGenerator(MTAConfigGenerator): + def __init__(self, policy_config, fixup=False): + self.fixup = fixup + MTAConfigGenerator.__init__(self, policy_config) + self.postfix_cf_file = self.find_postfix_cf() + self.wrangle_existing_config() + self.set_domainwise_tls_policies() + + def ensure_cf_var(self, var, ideal, also_acceptable): + """ + Ensure that existing postfix config @var is in the list of @acceptable + values; if not, set it to the ideal value. """ + + acceptable = [ideal] + also_acceptable + + l = [(num,line) for num,line in enumerate(self.cf) if line.startswith(var)] + if not any(l): + self.additions.append(var + " = " + ideal) + else: + values = map(parse_line, l) + if len(set(values)) > 1: + if self.fixup: + #print "Scheduling deletions:" + `values` + conflicting_lines = [num for num,_var,val in values] + self.deletions.extend(conflicting_lines) + self.additions.append(var + " = " + ideal) + else: + raise ExistingConfigError, "Conflicting existing config values " + `l` + val = values[0][2] + if val not in acceptable: + #print "Scheduling deletions:" + `values` + if self.fixup: + self.deletions.append(values[0][0]) + self.additions.append(var + " = " + ideal) + else: + raise ExistingConfigError, "Existing config has %s=%s"%(var,val) + + def wrangle_existing_config(self): + """ + Try to ensure/mutate that the config file is in a sane state. + Fixup means we'll delete existing lines if necessary to get there. + """ + self.additions = [] + self.deletions = [] + self.fn = self.find_postfix_cf() + self.raw_cf = open(self.fn).readlines() + self.cf = map(string.strip, self.raw_cf) + #self.cf = [line for line in cf if line and not line.startswith("#")] + + # Check we're currently accepting inbound STARTTLS sensibly + self.ensure_cf_var("smtpd_use_tls", "yes", []) + # Ideally we use it opportunistically in the outbound direction + self.ensure_cf_var("smtp_tls_security_level", "may", ["encrypt"]) + # Maximum verbosity lets us collect failure information + self.ensure_cf_var("smtp_tls_loglevel", "1", []) + # Inject a reference to our per-domain policy map + self.ensure_cf_var("smtp_tls_policy_maps", POLICY_CF_ENTRY, []) + + self.maybe_add_config_lines() + + def maybe_add_config_lines(self): + if not self.additions: + return + if self.fixup: + print "Deleting lines:", self.deletions + self.additions[:0]=["#","# New config lines added by STARTTLS Everywhere","#"] + new_cf_lines = "\n".join(self.additions) + "\n" + print "Adding to %s:" % self.fn + print new_cf_lines + if self.raw_cf[-1][-1] == "\n": sep = "" + else: sep = "\n" + + self.new_cf = "" + for num, line in enumerate(self.raw_cf): + if self.fixup and num in self.deletions: + self.new_cf += "# Line removed by STARTTLS Everywhere\n# " + line + else: + self.new_cf += line + self.new_cf += sep + new_cf_lines + + print self.new_cf + f = open(self.fn, "w") + f.write(self.new_cf) + f.close() + + def find_postfix_cf(self): + "Search far and wide for the correct postfix configuration file" + return os.path.join(POSTFIX_DIR,"main.cf") + + def set_domainwise_tls_policies(self): + self.policy_lines = [] + for domain, policy in self.policy_config.tls_policies.items(): + entry = domain + " encrypt" + if "min-tls-version" in policy: + entry += " " + policy["min-tls-version"] + self.policy_lines.append(entry) + + f = open(DEFAULT_POLICY_FILE, "w") + f.write("\n".join(self.policy_lines) + "\n") + f.close() + +if __name__ == "__main__": + import ConfigParser + c = ConfigParser.Config("starttls-everywhere.json") + pcgen = PostfixConfigGenerator(c, fixup=True) diff --git a/process-google-starttls-domains.py b/ProcessGoogleSTARTTLSDomains.py similarity index 100% rename from process-google-starttls-domains.py rename to ProcessGoogleSTARTTLSDomains.py diff --git a/README.md b/README.md index 79e0ed30d..2f0a410a1 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The basic file format will be JSON with comments (http://blog.getify.com/json-co // "timestamp": 1401414363, : also acceptable "author": "Electronic Frontier Foundation https://eff.org", "expires": "2014-06-06T14:30:16+00:00", - "security-policies": { + "tls-policies": { // These match on the MX domain. "*.yahoodns.net": { "require-valid-certificate": true, diff --git a/config.json b/config.json index 1a9034545..8d38696ff 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,7 @@ "timestamp": 1401093333, "author": "Electronic Frontier Foundation https://eff.org", "expires": 1404677353, "comment 2:": "epoch seconds", - "tls-policies": { + "security-policies": { "*.valid-example-recipient.com": { "min-tls-version": "TLSv1.1" } diff --git a/starttls-everywhere.json b/starttls-everywhere.json index 4c91866d5..d0e656186 100644 --- a/starttls-everywhere.json +++ b/starttls-everywhere.json @@ -1,5 +1,5 @@ { - "mx-domains": { + "tls-policies": { "*.mx.aol.com": { "min-tls-version": "TLSv1", "require-tls": true @@ -49,7 +49,7 @@ "require-tls": true } }, - "address-domains": { + "acceptable-mxs": { "wp.pl": { "accept-mx-domains": [ "*.wp.pl"