Remove non-plugin files

This commit is contained in:
Brad Warren 2017-08-29 10:42:31 -07:00
parent 142bc33545
commit dde0bf0821
24 changed files with 0 additions and 8538 deletions

4
.gitignore vendored
View file

@ -1,4 +0,0 @@
.*
*.orig
*.pyc
*.egg-info/

234
README.md
View file

@ -1,234 +0,0 @@
# STARTTLS Everywhere
## Example usage
**WARNING: this is a pre-alpha codebase. Do not run it on production
mailservers!!!**
If you have a Postfix server you're willing to endanger deliverability on, you
can try obtain a certificate with the [Let's Encrypt Python Client](https://github.com/letsencrypt/letsencrypt), note the directory it lives in below `/etc/letsencrypt/live` and then do:
```
git clone https://github.com/EFForg/starttls-everywhere
cd starttls-everywhere
# Promise you don't care if deliverability breaks on this mail server
letsencrypt-postfix/PostfixConfigGenerator.py examples/starttls-everywhere.json /etc/postfix /etc/letsencrypt/live/YOUR.DOMAIN.EXAMPLE.COM
```
This will:
* Ensure your mail server initiates STARTTLS encryption
* Install the Let's Encrypt cert in Postfix
* Enforce mandatory TLS to some major email domains
* Enforce minimum TLS versions to some major email domains
## Project status
STARTTLS Everywhere development is re-starting after a hiatus. Initial
objectives:
* Postfix configuration generation: working pre-alpha, not yet safe
* Email security database: working pre-alpha, definitely not yet safe
* Fully integrated Let's Encrypt client postfix plugin: in progress, not yet ready
* DANE support: none yet
* DEEP support: none yet
* SMTP-STS integration: none yet
* Direct mechanisms for mail domains to request inclusion: none yet
* Failure reporting mechanisms: early progress, not yet ready
* Mechanisms for secure multi-organization signature on the policy database:
none yet
* Support for mail servers other than Postfix: none yet
## Authors
Jacob Hoffman-Andrews <jsha@eff.org>,
Peter Eckersley <pde@eff.org>,
Daniel Wilcox <dmwilcox@gmail.com>,
Aaron Zauner <azet@azet.org>
## Mailing List
starttls-everywhere@eff.org, https://lists.eff.org/mailman/listinfo/starttls-everywhere
## Background
Most email transferred between SMTP servers (aka MTAs) is transmitted in the clear and trivially interceptable. Encryption of SMTP traffic is possible using the STARTTLS mechanism, which encrypts traffic but is vulnerable to trivial downgrade attacks.
To illustrate an easy version of this attack, suppose a network-based attacker `Mallory` notices that `Alice` has just uploaded message to her mail server. `Mallory` can inject a TCP reset (RST) packet during the mail server's next TLS negotiation with another mail server. Nearly all mail servers that implement STARTTLS do so in opportunistic mode, which means that they will retry without encryption if there is any problem with a TLS connection. So `Alice`'s message will be transmitted in the clear.
Opportunistic TLS in SMTP also extends to certificate validation. Mail servers commonly provide self-signed certificates or certificates with non-validatable hostnames, and senders commonly accept these. This means that if we say 'require TLS for this mail domain,' the domain may still be vulnerable to a man-in-the-middle using any key and certificate chosen by the attacker.
Even if senders require a valid certificate that matches the hostname of a mail host, a DNS MITM or Denial of Service is still possible. The sender, to find the correct target hostname, queries DNS for MX records on the recipient domain. Absent DNSSEC, the response can be spoofed to provide the attacker's hostname, for which the attacker holds a valid certificate.
STARTTLS by itself thwarts purely passive eavesdroppers. However, as currently deployed, it allows either bulk or semi-targeted attacks that are very unlikely to be detected. We would like to deploy both detection and prevention for such semi-targeted attacks.
## Goals
* Prevent RST attacks from revealing email contents in transit between major MTAs that support STARTTLS.
* Prevent MITM attacks at the DNS, SMTP, TLS, or other layers from revealing same.
* Zero or minimal decrease to deliverability rates unless network attacks are actually occurring.
* Create feedback-loops on targeted attacks and bulk surveilance in an opt-in, anonymized way.
## Non-goals
* Prevent fully-targeted exploits of vulnerabilities on endpoints or on mail hosts.
* Refuse delivery on the recipient side if sender does not negotiate TLS (this may be a future project).
* Develop a fully-decentralized solution.
* Initially we are not engineering to scale to all mail domains on the Internet, though we believe this design can be scaled as required if large numbers of domains publish policies to it.
## Motivating examples
* [Unnammed mobile broadband provider overwrites STARTTLS flag and commands to
prevent negotiating an encrypted connection](https://www.techdirt.com/articles/20141012/06344928801/revealed-isps-already-violating-net-neutrality-to-block-encryption-make-everyone-less-safe-online.shtml)
* [Unknown party removes STARTTLS flag from all SMTP connections leaving
Thailand](http://www.telecomasia.net/content/google-yahoo-smtp-email-severs-hit-thailand)
## Threat model
Attacker has control of routers on the path between two MTAs of interest. Attacker cannot or will not issue valid certificates for arbitrary names. Attacker cannot or will not attack endpoints. We are trying to protect confidentiality and integrity of email transmitted over SMTP between MTAs.
## Alternatives
Our goals can also be accomplished through use of [DNSSEC and DANE](http://tools.ietf.org/html/draft-ietf-dane-smtp-with-dane-10), which is certainly a more scalable solution. However, operators have been very slow to roll out DNSSEC supprt. We feel there is value in deploying an intermediate solution that does not rely on DNSSEC. This will improve the email security situation more quickly. It will also provide operational experience with authenticated SMTP over TLS that will make eventual rollout of DANE-based solutions easier.
## Detailed design
Senders need to know which target hosts are known to support STARTTLS, and how to authenticate them. Since the network cannot be trusted to provide this information, it must be communicated securely out-of-band. We will provide:
(a) a configuration file format to convey STARTTLS support for recipient domains,
(b) Python code (config-generator) to transform (a) into configuration files for popular MTAs., and
(c) a method to create and securely distribute files of type (a) for major email domains that that agree to be included, plus any other domains that proactively request to be included.
## File Format
The basic file format will be JSON with comments (http://blog.getify.com/json-comments/). Example:
{
// Canonical URL https://eff.org/starttls-everywhere/config -- redirects to latest version
"timestamp": "2014-06-06T14:30:16+00:00",
// "timestamp": 1401414363, : also acceptable
"author": "Electronic Frontier Foundation https://eff.org",
"expires": "2014-06-06T14:30:16+00:00",
"tls-policies": {
// These match on the MX domain.
"*.yahoodns.net": {
"require-valid-certificate": true,
}
"*.eff.org": {
"require-tls": true,
"min-tls-version": "TLSv1.1",
"enforce-mode": "enforce"
"accept-spki-hashes": [
"sha1/5R0zeLx7EWRxqw6HRlgCRxNLHDo=",
"sha1/YlrkMlC6C4SJRZSVyRvnvoJ+8eM="
]
}
"*.google.com": {
"require-valid-certificate": true,
"min-tls-version": "TLSv1.1",
"enforce-mode": "log-only",
"error-notification": "https://google.com/post/reports/here"
},
}
// Since the MX lookup is not secure, we list valid responses for each
// address domain, to protect against DNS spoofing.
"acceptable-mxs": {
"yahoo.com": {
"accept-mx-domains": ["*.yahoodns.net"]
}
"gmail.com": {
"accept-mx-domains": ["*.google.com"]
}
"eff.org": {
"accept-mx-domains": ["*.eff.org"]
}
}
}
A user of this file format may choose to accept multiple files. For instance, the EFF might provide an overall configuration covering major mail providers, and another organization might produce an overlay for mail providers in a specific country. If so, they override each other on a per-domain basis.
The _timestamp_ field is an integer number of epoch seconds from 00:00:00 UTC on 1 January 1970. When retrieving a fresh configuration file, config-generator should validate that the timestamp is greater than or equal to the version number of the file it already has.
There is no inline signature field. The configuration file should be distributed with authentication using an offline signing key.
Option 1: Plain JSON distributed with a signature using gpg --clearsign. Config-generator should validate the signature against a known GPG public key before extracting. The public key is part of the permanent system configuration, like the fetch URL.
Option 2: Git is a revision control system built on top of an authenticated, history-preserving file system. Let's use it as an authenticated, history preserving file system: valid versions of recipient policy files may be fetched and verified via signed git tags. [Here's an example shell recipe to do this.](https://gist.github.com/jsha/6230206e89759cc6e00d)
Config-generator should attempt to fetch the configuration file daily and transform it into MTA configs. If there is a retrieval failure, and the cached configuration file has an 'expires' time past the current date, an alert should be raised to the system operator and all existing configs from config-generator should be removed, reverting the MTA configuration to use opportunistic TLS for all domains.
**address-domains**
The _address-domains_ field maps from mail domains (the part of an address after the "@") onto a list of properties for that domain. Matching of mail domains is on an exact-match basis, not a subdomain basis. For instance, eff.org would be listed separately from lists.eff.org in the _address-domains_ section.
Currently the only property defined for _address-domains_ is _accept-mx-domains_, a list. If an MX lookup for a listed address domain returns a hostname that is not a subdomain of one of the domains listed in the _accept-mx-domains_ property, the MTA should fail delivery or log an advisory failure, as appropriate. Matching of MX hostnames against the _accept-mx-domains_ list is on a subdomain basis. For instance, if an MX record for yahoo.com lists mta7.am0.yahoodns.net, and the _accept-mx-domains_ property for yahoo.com is ["yahoodns.net"], that should be considered a match. All domains listed in any _accept-mx-domains_ list must correspond to an exactly matching field in the _mx-domains_ config section.
The _accept-mx-domains_ mechanism partially solves the problem of DNS MITM. It doesn't completely solve the problem, since an attacker might somehow control a different hostname under an acceptable domain, e.g. evil.yahoodns.net. But it strikes a balance between improving security and allowing mail operators to change configuration as needed. Some mail operators delegate their MX handling to a third-party provider (i.e. Google Apps for Your Domain). If those operators are included in STARTTLS Everywhere and wish to change providers, they will have to first send an update to their _accept-mx-domains_ to include their new provider.
**mx-domains**
The keys of this section are MX domains as described above for the _accept-mx-domains_ property. Each _mx-domain_ entry must be an exact match with an entry in one of the _accept-mx-domains_ lists provided. No _mx-domain_can be a subdomain of any other _mx-domain_in the configuration file. Fields in this section specify minimum security requirements that should be applied when connecting to any MX hostname that is a subdomain of the specified _mx-domain_.
Implicitly each _mx-domain_ listed has a property _require-tls: true_. MX domains that do not support TLS will not be listed. The only required property is _enforce-mode_, which must be either _log-only_ or _enforce_. If _enforce-mode_ is _log-only_, the generated configs will not stop mail delivery on policy failures, but will produce logging information.
If the _min-tls-version_ property is present, sending mail to domains under this policy should fail if the sending MTA cannot negotiate a TLS version equal to or greater than the listed version. Valid values are _TLSv1, TLSv1.1, and TLSv1.2._
_Require-valid-certificate_defaults to false. If the _require-valid-certificate_ property is 'true' for a given _mx-domain_ the certificate presented must be valid for a hostname that is subdomain of the _mx-domain_. Validity means all of these must be true:
1. The CN or a DNS entry under subjectAltName matches an appropriate hostname.
2. The certificate is unexpired.
3. There is a valid chain from the certificate to a root certificate included in [Mozilla's trust store](https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/included/) (available as [Debian package ca-certificates](https://packages.debian.org/sid/ca-certificates)).
The _accept-pinset_ field references an entry in the pinsets list, which has the same format and semantics as [Chrome's pinning list](https://src.chromium.org/chrome/trunk/src/net/http/transport_security_state_static.json). Most _mx-domain_s should specify a pinset that describes trust roots rather than leaf certificates, but both are possible. Pinning will only be added at the request of mail operators because it requires operators be careful when issuing new leaf certificates.
## Pinning and hostname verification
Like Chrome and Firefox we want to encourage pinning to a trusted root or intermediate rather than a leaf cert, to minimize spurious pinning failures when hosts rotate keys.
The other option is to automatically pin leaf certs as observed in the wild. This would be one solution to the hostname verification and self-signed certificate problem. However, it is a non-starter. Even if we expect mail operators to auto-update configuration on a daily basis, this approach cannot add new certs until they are observed in the wild. That means that any time an operator rotates keys on a mail server, there would be a significant window of time in which the new keys would be rejected.
We do not attempt to solve the self-signed certificate problem. For mail hosts with self-signed certificates, we can require TLS but will not require validation of the certificates. Such hosts should be encouraged to upgrade to a CA-signed certificate that can be validated by senders.
## Creating configuration
We have three options for creating the configuration file:
1. Ask mail operators to submit policies for their domains which we incorporate.
2. Manually curate a set of policies for the top `N` mail domains.
3. Programmatically create a set of policies by connecting to the top N mail domains.
For option (1), there's a bootstrapping problem: No one will opt in until it's useful; It won't be useful until people opt in. Option (1) does have the advantage that it's the only good way to get pinning directives.
For option (3) we'd be likely to pull in bad policies that could result in failed delivery.
We'll initially launch a demo using option (2), do some initial deployments to prove viability and delivery rate impact, and then start reaching out to operators to do option (1).
## Distribution
The configuration file will be provided at a long-term maintained URL. It will be signed using a key held offline on an airgapped machine or smartcard.
Since recipient mail servers may abruptly stop supporting TLS, we will request that mail operators set up auto-updating of the configuration file, with signature verification. This allows us to minimize the delivery impact of such events. However, config-generator should not auto-update its own code, since that would amount to auto-deployment of third party code, which some operators may not wish to do.
We may choose to implement a form of immutable log along the lines of certificate transparency. This would be appealing if we chose to use this mechanism to distribute expected leaf keys as a primary authentication mechanism, but as described in "Pinning and hostname verification," that's not a viable option. Instead we will rely on the CA ecosystem to do primary authentication, so an immutable log for this system is probably overkill, engineering-wise.
## Python code
Config-generator should parse input JSON and produce output configs for various mail servers. It should not be possible for any input JSON to cause arbitrary code execution or even any MTA config directives beyond the ones that specifically impact the decision to deliver or bounce based on TLS support. For instance, it must not be possible for config-generator to output a directive to forward mail from one domain to another. Config-generator will have the option to directly pull the latest config from a URL, or from a file on local disk distributed regularly from another system that has outside network access.
Config-generator will be manually updated by mail operators.
## Testing
We will create a reproducible test configuration that can be run locally and exercises each of the major cases: Enforce mode vs log mode; Enforced TLS negotiation, enforced MX hostname match, and enforced valid certificates.
Additionally, for ongoing monitoring of third-party deployments, we will create a canary mail domain that intentionally fails one of the tests but is included in the configuration file. For instance, starttls-canary.org would be listed in the configuration as requiring STARTTLS, but would not actually offer STARTTLS. Each time a mail operator commits to configuring STARTTLS Everywhere, we would request an account on their email domain from which to send automated daily email to starttls-canary.org. We should expect bounces. If such mail is successfully delivered to starttls-canary.org, that would indicate a configuration failure on the sending host, and we would manually notify the operator.
## Failure reporting
For the mail operator deploying STARTTLS Everywhere, we will provide log analysis scripts that can be used out-of-the-box to monitor how many delivery failures or would-be failures are due to STARTTLS Everywhere policies. These would be designed to run in a cron job or small opt-in daemon and send notices only when STARTTLS Everywhere-related failures exceed a certain percentage for any given recipient domains. For very high-volume mail operators, it would likely be necessary to adapt the analysis scripts to their own logging and analysis infrastructure.
For recipient domains who are listed in the STARTTLS Everywhere configuration, we would provide a configuration field to specify an email address or HTTPS URL to which that sender domains could send failure information. This would provide a mechanism for recipient domains to identify problems with their TLS deployment and fix them. The reported information should not contain any personal information, including email addresses. Example fields for failure reports: timestamps at minute granularity, target MX hostname, resolved MX IP address, failure type, certificate. Since failures are likely to come in batches, the error sending mechanism should batch them up and summarize as necessary to avoid flooding the recipient.

27
Vagrantfile vendored
View file

@ -1,27 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "hashicorp/precise32"
config.vm.define "sender" do |sender|
sender.vm.network "private_network", ip: "192.168.33.5"
sender.vm.hostname = "sender.example.com"
end
config.vm.define "valid" do |valid|
valid.vm.network "private_network", ip: "192.168.33.7"
valid.vm.hostname = "valid-example-recipient.com"
end
config.vm.synced_folder "vagrant-shared", "/vagrant"
config.vm.synced_folder "vagrant-shared/starttls-everywhere", "/vagrant/starttls-everywhere"
config.vm.provision :shell, path: "vagrant-bootstrap.sh"
config.vm.provider "virtualbox" do |vb|
# vb.gui = true
vb.customize ["modifyvm", :id, "--memory", "256"]
end
end

View file

@ -1,36 +0,0 @@
{
"timestamp": 1401414363,
"author": "Electronic Frontier Foundation https://eff.org",
"expires": "2015-08-01T12:00:00+08:00",
"tls-policies": {
".yahoodns.net": {
"require-valid-certificate": true
},
".eff.org": {
"require-tls": true,
"min-tls-version": "TLSv1.1",
"enforce-mode": "enforce",
"accept-spki-hashes": [
"sha1/5R0zeLx7EWRxqw6HRlgCRxNLHDo=",
"sha1/YlrkMlC6C4SJRZSVyRvnvoJ+8eM="
]
},
".google.com": {
"require-valid-certificate": true,
"min-tls-version": "TLSv1.1",
"enforce-mode": "log-only",
"error-notification": "https://google.com/post/reports/here"
}
},
"acceptable-mxs": {
"yahoo.com": {
"accept-mx-domains": [".yahoodns.net"]
},
"gmail.com": {
"accept-mx-domains": [".google.com"]
},
"eff.org": {
"accept-mx-domains": [".eff.org"]
}
}
}

View file

@ -1,17 +0,0 @@
{
"comment": "Canonical URL https://eff.org/starttls-everywhere/config -- redirects to latest version",
"timestamp": 1401093333,
"author": "Electronic Frontier Foundation https://eff.org",
"expires": 1404677353, "comment 2:": "epoch seconds",
"tls-policies": {
".valid-example-recipient.com": {
"min-tls-version": "TLSv1.1"
}
},
"acceptable-mxs": {
"valid-example-recipient.com": {
"accept-mx-domains": [ ".valid-example-recipient.com" ]
}
}
}

View file

@ -1,187 +0,0 @@
{
"acceptable-mxs": {
"163.com": {
"accept-mx-domains": [
".163.com"
]
},
"aol.com": {
"accept-mx-domains": [
".aol.com"
]
},
"craigslist.org": {
"accept-mx-domains": [
".craigslist.org"
]
},
"gmail.com": {
"accept-mx-domains": [
".google.com"
]
},
"hotmail.com": {
"accept-mx-domains": [
".outlook.com"
]
},
"icloud.com": {
"accept-mx-domains": [
".icloud.com"
]
},
"live.com": {
"accept-mx-domains": [
".outlook.com"
]
},
"mac.com": {
"accept-mx-domains": [
".icloud.com"
]
},
"me.com": {
"accept-mx-domains": [
".icloud.com"
]
},
"msn.com": {
"accept-mx-domains": [
".outlook.com"
]
},
"naver.com": {
"accept-mx-domains": [
".naver.com"
]
},
"outlook.com": {
"accept-mx-domains": [
".outlook.com"
]
},
"qq.com": {
"accept-mx-domains": [
".qq.com"
]
},
"rocketmail.com": {
"accept-mx-domains": [
".yahoo.com"
]
},
"rogers.com": {
"accept-mx-domains": [
".yahoo.com"
]
},
"salesforce.com": {
"accept-mx-domains": [
".psmtp.com"
]
},
"sbcglobal.net": {
"accept-mx-domains": [
".yahoo.com"
]
},
"shaw.ca": {
"accept-mx-domains": [
".shaw.ca"
]
},
"sympatico.ca": {
"accept-mx-domains": [
".outlook.com"
]
},
"t-online.de": {
"accept-mx-domains": [
".t-online.de"
]
},
"wp.pl": {
"accept-mx-domains": [
".wp.pl"
]
},
"yahoo.com": {
"accept-mx-domains": [
".yahoo.com"
]
},
"yahoogroups.com": {
"accept-mx-domains": [
".yahoo.com"
]
},
"yandex.ru": {
"accept-mx-domains": [
".yandex.ru"
]
},
"ymail.com": {
"accept-mx-domains": [
".yahoo.com"
]
}
},
"tls-policies": {
".163.com": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".aol.com": {
"min-tls-version": "TLSv1",
"require-tls": true
},
".craigslist.org": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".google.com": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".icloud.com": {
"min-tls-version": "TLSv1",
"require-tls": true
},
".naver.com": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".outlook.com": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".psmtp.com": {
"min-tls-version": "TLSv1",
"require-tls": true
},
".qq.com": {
"min-tls-version": "TLSv1",
"require-tls": true
},
".shaw.ca": {
"min-tls-version": "TLSv1",
"require-tls": true
},
".t-online.de": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".wp.pl": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".yahoo.com": {
"min-tls-version": "TLSv1.1",
"require-tls": true
},
".yandex.ru": {
"min-tls-version": "TLSv1.1",
"require-tls": true
}
}
}

View file

@ -1,569 +0,0 @@
#!/usr/bin/env python
from datetime import datetime
from dateutil import parser as dateutil_parser
import collections
import json
import logging
import pprint
"""Idea here being to start with something that is decomposed so it's easier to
make do json in *and* out, differences between configs and config extension.
"""
#TODO scope logging and handlers better, control verbosity by command line flags
logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler())
def parse_bool_from_json(value, attr_name):
if value in ('true', '1', 1, 'yes'):
bool_value = True
elif value in ('false', '0', 0, 'no'):
bool_value = False
elif value in (True, False):
bool_value = value
else:
raise ConfigError('Config value %s is an invalid boolean value.' % attr_name)
return bool_value
def parse_timestamp(value, attr_name):
if isinstance(value, datetime):
return value
try:
ts = int(value)
return datetime.fromtimestamp(ts)
except (TypeError, ValueError):
pass
try:
return dateutil_parser.parse(value)
except (TypeError, ValueError):
raise ConfigError('Config value %s is an invalid date or timestamp.' % attr_name)
def verify_member_of(value, member_list, attr_name):
if value not in member_list:
raise ConfigError('Config value "%s" must be one of (%s)' % (
attr_name, ', '.join(member_list))
)
return value
def verify_string(value, attr_name, max_length=200):
if not isinstance(value, (str, unicode)):
raise ConfigError('Config value %s must be a string.' % attr_name)
if len(value) > max_length:
raise ConfigError('Config value %s is too long.' % attr_name)
return value
def to_dict(config_dict):
"""Cleans up BaseConfig children to be serialized."""
d = {}
for key, val in config_dict.iteritems():
if isinstance(val, BaseConfig):
d[key] = to_dict(val._data)
elif isinstance(val, datetime):
d[key] = val.strftime('%Y-%m-%dT%H:%M:%S%z')
elif isinstance(val, dict):
d[key] = to_dict(val)
else:
d[key] = val
return d
class BaseConfig(object):
"""Top level config class for common methods.
Requirements for using class:
- list all properties with getters *and* setters in class
variable 'config_properties'
- __init__ of child classes must be callable with *only*
keyword arguments to allow method calls to update to create
a new config
... more ...
"""
config_properties = []
def __init__(self):
# container for validated properties with JSON names
self._data = {}
def __repr__(self):
s = '< %s %s >' % (self.__class__.__name__,
pprint.pformat(self._data))
return s
def update(self, newer_config, merge=False, **kwargs):
"""Create a fresh config combining the new and old configs.
It does this by iterating over the 'config_properties' class
attribute which contains names of property attributes for the config.
Two methods of combining configs are possible, an 'update' and
a 'merge', the latter set by the keyword argument 'merge=True'.
An update overrides older values with new values -- even if those
new values are None. Update will remove values that are present in
the old config if they are not present in the new config.
A merge by comparison will allow old values to persist if they are
not specified in the new config. This can be used for end-user
customizations to override specific settings without having to re-create
large portions of a config to override it.
Arguments:
newer_config: A config object to combine with the current config.
merge: Allows old values not overridden to survive into the fresh config.
Returns:
A config object of the same sort as called upon.
"""
# removed 'merge' kw arg - and it was passed to constructor
# make a note to not do that, consume it on the param list
fresh_config = self.__class__(**kwargs)
logger.debug('from parent update kwargs %s' % kwargs)
logger.debug('from parent update merge %s' % merge)
if not isinstance(newer_config, self.__class__):
raise ConfigError('Attempting to update a %s with a %s' % (
self.__class__,
newer_config.__class__))
for prop_name in self.config_properties:
# get the specified property off of the current class
prop = self.__class__.__dict__.get(prop_name)
assert prop
new_value = prop.fget(newer_config)
old_value = prop.fget(self)
if new_value is not None:
prop.fset(fresh_config, new_value)
elif merge and old_value is not None:
prop.fset(fresh_config, old_value)
return fresh_config
def merge(self, newer_config, **kwargs):
"""Combines configs and keeps old values if they are not overridden.
See docstring for 'update' method for more details.
Arguments:
newer_config: A config object to combine with the current config.
merge: Allows old values not overridden to survive into the fresh config.
Returns:
A config object of the same sort as called upon.
"""
kwargs['merge'] = True
logger.debug('from parent merge: %s' % kwargs)
return self.update(newer_config, **kwargs)
def to_json(self):
d = to_dict(self._data)
return json.dumps(d)
def write_to_json_file(self, json_filename, f_open=open):
data = self.to_json()
try:
with f_open(json_filename, 'w') as f:
f.write(data)
except IOError:
raise
def load_from_json_file(self, json_filename, f_open=open):
try:
with f_open(json_filename, 'r') as f:
json_str = f.read()
json_dict = json.loads(json_str)
except IOError:
raise
except ValueError:
raise ConfigError('No valid JSON found in file: %s' % json_filename)
self.from_json_dict(json_dict)
def from_json_dict(self, json_dict):
raise NotImplmented('BaseConfig should not be populated.')
class Config(BaseConfig):
"""Config container for StartTLS Everywhere configuration.
Intended as a simple container that unifies where validatation occurs,
and is capable of comparing configs to warn of things like changing
certificate fingerprints from one scan to the next.
There is a one to one mapping of the object attributes to the JSON
object keys, albeit with dashes replaced with underscores.
"""
def __init__(self):
super(self.__class__, self).__init__()
self._data['tls-policies'] = {}
self._data['acceptable-mxs'] = {}
def __add__(self, other_config):
"""Allow addition but not really of *full* configs, need to flesh that out."""
#TODO add this
raise NotImplemented
def update(self, other_config):
"""Update properties of config from a 'newer' config and force verification."""
#TODO add this
new_config = Config()
raise NotImplemented
def from_json_dict(self, json_dict):
"""Assign JSON data to Config properties and declare sub-objects.
Let's property verification methods do the heavy lifting and mostly
maps between the JSON config names and attributes. Keeps track of
unused variables and warns about them.
"""
for key, val in json_dict.iteritems():
if key == 'author':
self.author = val
elif key == 'comment':
self.comment = val
elif key == 'expires':
self.expires = val
elif key == 'timestamp':
self.timestamp = val
elif key == 'tls-policies':
self.make_tls_policy_dict(val)
elif key == 'acceptable-mxs':
self.make_acceptable_mxs_dict(val)
else:
logger.warn('Unknown attribute "%s", skipping' % key)
@property
def author(self):
return self._data.get('author')
@author.setter
def author(self, value):
self._data['author'] = verify_string(value, 'author')
@property
def comment(self):
return self._data.get('comment')
@comment.setter
def comment(self, value):
self._data['comment'] = verify_string(value, 'comment')
@property
def expires(self):
return self._data.get('expires')
@expires.setter
def expires(self, value):
self._data['expires'] = parse_timestamp(value, 'expires')
@property
def timestamp(self):
return self._data.get('timestamp')
@timestamp.setter
def timestamp(self, value):
self._data['timestamp'] = parse_timestamp(value, 'timestamp')
@property
def tls_policies(self):
return self._data.get('tls-policies')
@property
def acceptable_mxs(self):
return self._data.get('acceptable-mxs')
def make_tls_policy_dict(self, policy_dict):
tls_policy_dict = self.tls_policies
for domain_suffix, settings in policy_dict.iteritems():
new_domain_policy = TLSPolicy(domain_suffix)
try:
new_domain_policy.from_json_dict(settings)
except ConfigError as e:
raise
tls_policy_dict[domain_suffix] = new_domain_policy
def get_tls_policy(self, mx_domain):
return self.tls_policies.get(mx_domain)
def make_acceptable_mxs_dict(self, mxs_dict):
acceptable_mxs_dict = self._data['acceptable-mxs']
for domain, settings in mxs_dict.iteritems():
new_domain_policy = AcceptableMX(domain)
try:
new_domain_policy.from_json_dict(settings)
except ConfigError as e:
raise
acceptable_mxs_dict[domain] = new_domain_policy
def get_address_domains(self, mx_hostname, mx_to_domain_map):
"""Do a fuzzy DNS host match on provided map to get lists of policies.
Args:
mx_hostname (string): The hostname from an MX record.
mx_to_domain_map: Mapping from MX hosts to AcceptableMX
policies, the same AcceptableMX policy may occur more
than once. e.g. {'mx_host3': set(AcceptableMX, ...)}
The map can be generated by Config.get_mx_to_domain_policy_map.
Returns:
The set containing all AcceptableMX policies that list the
provided MX host as viable.
"""
labels = mx_hostname.split(".")
for n in range(1, len(labels)):
parent = "." + ".".join(labels[n:])
if parent in mx_to_domain_map:
return mx_to_domain_map[parent]
return None
def get_mx_to_domain_policy_map(self):
"""Create mapping of MX hostnames to sets of AcceptableMX policies.
Generate a dictionary that is typically used in log analysis
(e.g. if your MTA logs interact with beta.innotech.com you use
this mapping to tell you it used the innotech.com AcceptableMX
policy or policies). There are of course complications.
"""
# create reverse mapping dictionary as well for auditing
# and reviewing logs
mx_to_domain_policy = collections.defaultdict(set)
for mx_host, domain_policy in self.get_all_mx_items():
existing_mx_policies = mx_to_domain_policy.get(mx_host)
if existing_mx_policies:
existing_domains = [ e.domain for e in existing_mx_policies ]
if domain_policy.domain not in existing_domains:
#TODO plenty of room to enforce a security policy here
# this is also the case of google apps personal domains
msg = ('Attempting to add domain policy (%s) for MX host but MX'
' host already has a domain policy (%s), appending...')
logger.debug(msg % (domain_policy.domain,
', '.join(existing_domains)))
mx_to_domain_policy[mx_host].add(domain_policy)
return mx_to_domain_policy
def get_all_mx_items(self):
"""Iterate over (mx_host, mx_policy) - be sure to dedup!"""
all_mx_items = []
for policy in self.acceptable_mxs.values():
accepted_mxs = policy.accept_mx_domains
all_mx_items.extend([(mx_host, policy)
for mx_host in accepted_mxs])
return all_mx_items
def get_all_mx_hosts(self):
all_mx_hosts = []
[ all_mx_hosts.extend(domain_policy.acceptable_mxs)
for domain_policy in self.acceptable_mxs.values() ]
return all_mx_hosts
def is_valid(self):
#TODO implement checks to make sure domains don't overlap
#TODO add debug logging for troubleshooting sake
for mx_config in self.acceptable_mxs.values():
if not mx_config.is_valid():
return False
for domain_suffix in mx_config.accept_mx_domains:
# check to make sure every accepted MX has a TLS policy
if not domain_suffix in self.tls_policies:
return False
all_mx_hosts = self.get_all_mx_hosts()
for domain_suffix, tls_config in self.tls_policies.iteritems():
if not tls_config.is_valid():
return False
# make sure no unclaimed TLS policies have made their way in
if domain_suffix not in all_mx_hosts:
return False
return True
class TLSPolicy(BaseConfig):
ENFORCE_MODES = ('enforce', 'log-only')
TLS_VERSIONS = ('TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3')
config_properties = ['comment', 'enforce_mode', 'min_tls_version',
'require_tls', 'require_valid_certificate']
def __init__(self, domain_suffix=None):
super(self.__class__, self).__init__()
self.domain_suffix = domain_suffix
#TODO add support for two designed but yet unsupported attrs
# self._data['accept-spki-hashs'] = None
# self._data['error-notification'] = None
def from_json_dict(self, json_dict):
for key, val in json_dict.iteritems():
if key == 'comment':
self.comment = val
elif key == 'enforce-mode':
self.enforce_mode = val
elif key == 'min-tls-version':
self.min_tls_version = val
elif key == 'require-tls':
self.require_tls = val
elif key == 'require-valid-certificate':
self.require_valid_certificate = val
else:
logger.warn('Unknown key %s' % key)
def is_valid(self):
"""Do simple check that config contains all required values.
Should find a way to expose easily which config values
are required, at least place in error messages such that
incomplete configs will expose it.
"""
required_attrs = ('enforce-mode', 'min-tls-version',
'require-tls')
values_set = [self._data.get(attr) for attr in required_attrs]
if not all(values_set):
return False
else:
return True
def update(self, newer_policy, **kwargs):
if not kwargs.get('domain_suffix'):
kwargs['domain_suffix'] = self.domain_suffix
fresh_policy = super(self.__class__, self).update(newer_policy,
**kwargs)
logger.debug('from TLS child update %s' % kwargs)
return fresh_policy
def merge(self, newer_policy, **kwargs):
logger.debug('from TLS child merge: %s' % kwargs)
fresh_policy = super(self.__class__, self).merge(newer_policy,
domain_suffix=self.domain_suffix)
return fresh_policy
@property
def comment(self):
return self._data.get('comment')
@comment.setter
def comment(self, value):
self._data['comment'] = verify_string(value, 'comment')
@property
def enforce_mode(self):
return self._data.get('enforce-mode')
@enforce_mode.setter
def enforce_mode(self, value):
self._data['enforce-mode'] = verify_member_of(value, self.ENFORCE_MODES, 'enforce-mode')
@property
def min_tls_version(self):
return self._data.get('min-tls-version')
@min_tls_version.setter
def min_tls_version(self, value):
"""TODO: Should this be dealing only with strings processed by map ... lower()?"""
tls_versions = [ver.lower() for ver in self.TLS_VERSIONS]
tls_versions.extend(self.TLS_VERSIONS)
self._data['min-tls-version'] = verify_member_of(value, tls_versions, 'min-tls-version')
@property
def require_tls(self):
return self._data.get('require-tls')
@require_tls.setter
def require_tls(self, value):
self._data['require-tls'] = parse_bool_from_json(value, 'require-tls')
@property
def require_valid_certificate(self):
return self._data.get('require-valid-certificate')
@require_valid_certificate.setter
def require_valid_certificate(self, value):
self._data['require-valid-certificate'] = parse_bool_from_json(value, 'require-valid-certificate')
class AcceptableMX(BaseConfig):
"""Holds acceptable MX domain suffixes for a single mail serving domain.
Such as for gmail.com that single mail serving suffix domain is:
gmail-smtp-in.l.google.com.
Configuration of the acceptable MX suffix domains must match up with TLS policies
for the suffix domains.
"""
def __init__(self, domain=None):
super(self.__class__, self).__init__()
self.domain = domain
self._data['accept-mx-domains'] = []
@property
def accept_mx_domains(self):
return self._data.get('accept-mx-domains')
def add_acceptable_mx(self, domain_suffix):
unique_domain_suffixes = set(self._data['accept-mx-domains'])
unique_domain_suffixes.add(domain_suffix)
self._data['accept-mx-domains'] = list(unique_domain_suffixes)
@property
def comment(self):
return self._data.get('comment')
@comment.setter
def comment(self, value):
self._data['comment'] = verify_string(value, 'comment')
def is_valid(self):
"""Check to make sure there is one acceptable domain suffix.
This will need to be updated once we can actually test and support
for more than one acceptable domain suffix.
TODO: could make this object double check the data it is given with
DNS queries.
"""
if len(self._data['accept-mx-domains']) != 1:
return False
else:
return True
def from_json_dict(self, json_dict):
for key, val in json_dict.iteritems():
if key == 'accept-mx-domains':
if isinstance(val, list):
for domain_suffix in val:
self.add_acceptable_mx(domain_suffix)
else:
self.add_acceptable_mx(val)
elif key == 'comment':
self.comment = val
else:
logger.warn('warning: unknown key %s' % key)
def update(self, newer_policy, **kwargs):
logger.debug('from MX child update got %s' % kwargs)
if not kwargs.get('domain'):
kwargs['domain'] = self.domain
fresh_policy = super(self.__class__, self).update(newer_policy,
**kwargs)
if kwargs.get('merge'):
new_accepted_mxs = set(self.accept_mx_domains)
new_accepted_mxs = new_accepted_mxs.union(newer_policy.accept_mx_domains)
else:
new_accepted_mxs = newer_policy.accept_mx_domains
for domain in new_accepted_mxs:
fresh_policy.add_acceptable_mx(domain)
return fresh_policy
def merge(self, newer_policy, **kwargs):
logger.debug('from MX child merge: %s' % kwargs)
fresh_policy = super(self.__class__, self).merge(newer_policy,
**kwargs)
return fresh_policy
class ConfigError(ValueError):
def __init__(self, message):
super(self.__class__, self).__init__(message)

View file

@ -1,104 +0,0 @@
#!/usr/bin/env python
import argparse
import collections
import os
import re
import sys
import time
import Config
TIME_FORMAT = "%b %d %H:%M:%S"
# TODO: There's more to be learned from postfix logs! Here's one sample
# observed during failures from the sender vagrant vm:
# Jun 6 00:21:31 precise32 postfix/smtpd[3648]: connect from localhost[127.0.0.1]
# Jun 6 00:21:34 precise32 postfix/smtpd[3648]: lost connection after STARTTLS from localhost[127.0.0.1]
# Jun 6 00:21:34 precise32 postfix/smtpd[3648]: disconnect from localhost[127.0.0.1]
# Jun 6 00:21:56 precise32 postfix/master[3001]: reload -- version 2.9.6, configuration /etc/postfix
# Jun 6 00:22:01 precise32 postfix/pickup[3674]: AF3B6480475: uid=0 from=<root>
# Jun 6 00:22:01 precise32 postfix/cleanup[3680]: AF3B6480475: message-id=<20140606002201.AF3B6480475@sender.example.com>
# Jun 6 00:22:01 precise32 postfix/qmgr[3673]: AF3B6480475: from=<root@sender.example.com>, size=576, nrcpt=1 (queue active)
# Jun 6 00:22:01 precise32 postfix/smtp[3682]: SSL_connect error to valid-example-recipient.com[192.168.33.7]:25: -1
# Jun 6 00:22:01 precise32 postfix/smtp[3682]: warning: TLS library problem: 3682:error:140740BF:SSL routines:SSL23_CLIENT_HELLO:no protocols available:s23_clnt.c:381:
# Jun 6 00:22:01 precise32 postfix/smtp[3682]: AF3B6480475: to=<vagrant@valid-example-recipient.com>, relay=valid-example-recipient.com[192.168.33.7]:25, delay=0.06, delays=0.03/0.03/0/0, dsn=4.7.5, status=deferred (Cannot start TLS: handshake failure)
#
# Also:
# Oct 10 19:12:13 sender postfix/smtp[1711]: 62D3F481249: to=<vagrant@valid-example-recipient.com>, relay=valid-example-recipient.com[192.168.33.7]:25, delay=0.07, delays=0.03/0.01/0.03/0, dsn=4.7.4, status=deferred (TLS is required, but was not offered by host valid-example-recipient.com[192.168.33.7])
def get_counts(input, config, earliest_timestamp):
seen_trusted = False
counts = collections.defaultdict(lambda: collections.defaultdict(int))
tls_deferred = collections.defaultdict(int)
# Typical line looks like:
# Jun 12 06:24:14 sender postfix/smtp[9045]: Untrusted TLS connection established to valid-example-recipient.com[192.168.33.7]:25: TLSv1.1 with cipher AECDH-AES256-SHA (256/256 bits)
# indicate a problem that should be alerted on.
# ([^[]*) <--- any group of characters that is not "["
# Log lines for when a message is deferred for a TLS-related reason. These
deferred_re = re.compile("relay=([^[ ]*).* status=deferred.*TLS")
# Log lines for when a TLS connection was successfully established. These can
# indicate the difference between Untrusted, Trusted, and Verified certs.
connected_re = re.compile("([A-Za-z]+) TLS connection established to ([^[]*)")
mx_to_domain_mapping = config.get_mx_to_domain_policy_map()
timestamp = 0
for line in sys.stdin:
timestamp = time.strptime(line[0:15], TIME_FORMAT)
if timestamp < earliest_timestamp:
continue
deferred = deferred_re.search(line)
connected = connected_re.search(line)
if connected:
validation = connected.group(1)
mx_hostname = connected.group(2).lower()
if validation == "Trusted" or validation == "Verified":
seen_trusted = True
address_domains = config.get_address_domains(mx_hostname, mx_to_domain_mapping)
if address_domains:
domains_str = [ a.domain for a in address_domains ]
d = ', '.join(domains_str)
counts[d][validation] += 1
counts[d]["all"] += 1
elif deferred:
mx_hostname = deferred.group(1).lower()
tls_deferred[mx_hostname] += 1
return (counts, tls_deferred, seen_trusted, timestamp)
def print_summary(counts):
for mx_hostname, validations in counts.items():
for validation, validation_count in validations.items():
if validation == "all":
continue
print mx_hostname, validation, validation_count / validations["all"], "of", validations["all"]
if __name__ == "__main__":
arg_parser = argparse.ArgumentParser(description='Detect delivery problems'
' in Postfix log files that may be caused by security policies')
arg_parser.add_argument('-c', action="store_true", dest="cron", default=False)
arg_parser.add_argument("policy_file", nargs='?',
default=os.path.join("examples", "starttls-everywhere.json"),
help="STARTTLS Everywhere policy file")
args = arg_parser.parse_args()
config = Config.Config()
config.load_from_json_file(args.policy_file)
last_timestamp_processed = 0
timestamp_file = '/tmp/starttls-everywhere-last-timestamp-processed.txt'
if os.path.isfile(timestamp_file):
last_timestamp_processed = time.strptime(open(timestamp_file).read(), TIME_FORMAT)
(counts, tls_deferred, seen_trusted, latest_timestamp) = get_counts(sys.stdin, config, last_timestamp_processed)
with open(timestamp_file, "w") as f:
f.write(time.strftime(TIME_FORMAT, latest_timestamp))
# If not running in cron, print an overall summary of log lines seen from known hosts.
if not args.cron:
print_summary(counts)
if not seen_trusted:
print 'No Trusted connections seen! Probably need to install a CAfile.'
if len(tls_deferred) > 0:
print "Some mail was deferred due to TLS problems:"
for (k, v) in tls_deferred.iteritems():
print "%s: %s" % (k, v)

View file

@ -1,132 +0,0 @@
#!/usr/bin/env python
import copy
import itertools
import logging
import unittest
import Config
logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler())
class TestTLSPolicy(unittest.TestCase):
def setUp(self):
self.old_config = Config.TLSPolicy(domain_suffix='.eff.org')
self.old_config.comment = 'Testing EFF.org TLS policy'
self.old_config.require_tls = True
self.old_config.require_valid_certificate = False
self.old_config.min_tls_version = 'TLSv1'
self.old_config.enforce_mode = 'log-only'
self.new_config = Config.TLSPolicy(domain_suffix='.eff.org')
self.new_config.require_valid_certificate = True
self.new_config.min_tls_version = 'TLSv1.2'
self.new_config.enforce_mode = 'enforce'
def testUpdateDropsOldSettings(self):
logger.debug('old: %s' % self.old_config)
logger.debug('new: %s' % self.new_config)
tls_policy = self.old_config.update(self.new_config)
logger.debug('just generated: %s' % tls_policy)
self.assertFalse(any([tls_policy.require_tls, tls_policy.comment]))
def testMergeKeepsOldSettings(self):
logger.debug('old: %s' % self.old_config)
logger.debug('new: %s' % self.new_config)
tls_policy = self.old_config.merge(self.new_config, merge=True)
logger.debug('just generated: %s' % tls_policy)
self.assertTrue(all([tls_policy.require_tls, tls_policy.comment]))
def testUpdateGetsNameSet(self):
tls_policy = self.old_config.update(self.new_config)
self.assertEquals(tls_policy.domain_suffix, self.old_config.domain_suffix)
class TestAcceptableMX(unittest.TestCase):
def setUp(self):
self.old_config = Config.AcceptableMX(domain='eff.org')
self.old_config.add_acceptable_mx('.eff.org')
def testUpdateDropsOldMXs(self):
new_bogus_mx = '.testing.eff.org'
new_config = Config.AcceptableMX(domain='eff.org')
new_config.add_acceptable_mx(new_bogus_mx)
updated_config = self.old_config.update(new_config)
self.assertNotIn('.eff.org', updated_config.accept_mx_domains)
def testMergeKeepsOldMXs(self):
new_bogus_mx = '.testing.eff.org'
new_config = Config.AcceptableMX(domain='eff.org')
new_config.add_acceptable_mx(new_bogus_mx)
updated_config = self.old_config.merge(new_config)
self.assertListEqual(sorted(['.eff.org', '.testing.eff.org']),
sorted(updated_config.accept_mx_domains))
def testUpdateGetsNameSet(self):
new_policy = Config.AcceptableMX(domain=self.old_config.domain)
mx_policy = self.old_config.update(new_policy)
self.assertEquals(mx_policy.domain, self.old_config.domain)
class TestConfig(unittest.TestCase):
"""Test entire configuration.
Currently lower coverage is being obtained since string sets are
being compared rather than returned objects. Comparison logic for
the config objects isn't clear yet and proof that they function is enough.
"""
def setUp(self):
self.config = Config.Config()
domain_policies = self.config._data['acceptable-mxs']
self.mail_domains = ['gmail.com', 'yahoo.com', 'hotmail.com', '123.cn', 'qq.com']
for domain in self.mail_domains:
new = Config.AcceptableMX(domain=domain)
new.add_acceptable_mx('.' + domain)
domain_policies[domain] = new
def testGetAllMxItems(self):
"""Make sure the basic use case of get_all_mx_items functions."""
# [ ('.gmail.com', 'gmail.com'), ('.yahoo.com', 'yahoo.com'), ... ]
control_data = [ ('.' + domain, domain) for domain in self.mail_domains ]
test_data = [ (mx, p.domain) for mx, p in self.config.get_all_mx_items() ]
self.assertListEqual(sorted(test_data), sorted(control_data))
def testGetAllMxItemsMultiMX(self):
config = copy.deepcopy(self.config)
domain_policy = config.acceptable_mxs.get('gmail.com')
# deal with reality, mail.google.com
domain_policy.add_acceptable_mx('.mail.google.com')
control_data = [ ('.' + domain, domain) for domain in self.mail_domains ]
control_data.append(('.mail.google.com', 'gmail.com'))
test_data = [ (mx, p.domain) for mx, p in config.get_all_mx_items() ]
self.assertListEqual(sorted(test_data), sorted(control_data))
def testGetMXtoDomainPolicy(self):
control_data = dict([ ('.' + domain, set([domain]))
for domain in self.mail_domains ])
test_data = {}
for mx, pset in self.config.get_mx_to_domain_policy_map().items():
policy_list = [ p.domain for p in pset ]
test_data[mx] = set(policy_list)
self.assertDictEqual(test_data, control_data)
def testGetMXtoDomainPolicyMultiMX(self):
config = copy.deepcopy(self.config)
domain_policy = config.acceptable_mxs.get('gmail.com')
domain_policy.add_acceptable_mx('.mail.google.com')
control_data = dict([ ('.' + domain, set([domain]))
for domain in self.mail_domains ])
control_data['.mail.google.com'] = set(['gmail.com'])
test_data = {}
for mx, pset in config.get_mx_to_domain_policy_map().items():
policy_list = [ p.domain for p in pset ]
test_data[mx] = set(policy_list)
self.assertDictEqual(test_data, control_data)
if __name__ == '__main__':
unittest.main()

View file

@ -1,4 +0,0 @@
dnspython
publicsuffix
m2crypto
dateutils

View file

@ -1,32 +0,0 @@
163.com
aol.com
bigpond.com
comcast.net
craigslist.org
facebook.com
gmail.com
gmx.de
hotmail.com
icloud.com
live.com
mac.com
me.com
msn.com
naver.com
outlook.com
qq.com
rocketmail.com
rogers.com
salesforce.com
sbcglobal.net
shaw.ca
sympatico.ca
t-online.de
ukr.net
vtext.com
web.de
wp.pl
yahoo.com
yahoogroups.com
yandex.ru
ymail.com

File diff suppressed because it is too large Load diff

View file

@ -1,191 +0,0 @@
#!/usr/bin/env python
import sys
import os
import errno
import smtplib
import socket
import subprocess
import re
import json
import collections
import dns.resolver
from M2Crypto import X509
from publicsuffix import PublicSuffixList
public_suffix_list = PublicSuffixList()
CERTS_OBSERVED = 'certs-observed'
def mkdirp(path):
try:
os.makedirs(path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
def extract_names(pem):
"""Return a set of DNS subject names from PEM-encoded leaf cert."""
leaf = X509.load_cert_string(pem, X509.FORMAT_PEM)
subj = leaf.get_subject()
# Certs have a "subject" identified by a Distingushed Name (DN).
# Host certs should also have a Common Name (CN) with a DNS name.
common_names = subj.get_entries_by_nid(subj.nid['CN'])
common_names = [name.get_data().as_text() for name in common_names]
try:
# The SAN extension allows one cert to cover multiple domains
# and permits DNS wildcards.
# http://www.digicert.com/subject-alternative-name.htm
# The field is a comma delimited list, e.g.:
# >>> twitter_cert.get_ext('subjectAltName').get_value()
# 'DNS:www.twitter.com, DNS:twitter.com'
alt_names = leaf.get_ext('subjectAltName').get_value()
alt_names = alt_names.split(', ')
alt_names = [name.partition(':') for name in alt_names]
alt_names = [name for prot, _, name in alt_names if prot == 'DNS']
except:
alt_names = []
return set(common_names + alt_names)
def tls_connect(mx_host, mail_domain):
"""Attempt a STARTTLS connection with openssl and save the output."""
if supports_starttls(mx_host):
# smtplib doesn't let us access certificate information,
# so shell out to openssl.
try:
output = subprocess.check_output(
"""openssl s_client \
-starttls smtp -connect %s:25 -showcerts </dev/null \
2>/dev/null
""" % mx_host, shell = True)
except subprocess.CalledProcessError:
print "Failed s_client"
return
# Save a copy of the certificate for later analysis
with open(os.path.join(CERTS_OBSERVED, mail_domain, mx_host), "w") as f:
f.write(output)
def valid_cert(filename):
"""Return true if the certificate is valid.
Note: CApath must have hashed symlinks to the trust roots.
TODO: Include the -attime flag based on file modification time."""
if open(filename).read().find("-----BEGIN CERTIFICATE-----") == -1:
return False
try:
# The file contains both the leaf cert and any intermediates, so we pass it
# as both the cert to validate and as the "untrusted" chain.
output = subprocess.check_output("""openssl verify -CApath /home/jsha/mozilla/ -purpose sslserver \
-untrusted "%s" \
"%s"
""" % (filename, filename), shell = True)
return True
except subprocess.CalledProcessError:
return False
def check_certs(mail_domain):
"""
Return "" if any certs for any mx domains pointed to by mail_domain
were invalid, and a public suffix for one if they were all valid
"""
dir = os.path.join(CERTS_OBSERVED, mail_domain)
if not os.path.exists(dir):
collect(mail_domain)
names = set()
for mx_hostname in os.listdir(dir):
filename = os.path.join(dir, mx_hostname)
if not valid_cert(filename):
return ""
else:
new_names = extract_names_from_openssl_output(filename)
new_names = set(public_suffix_list.get_public_suffix(n) for n in new_names)
names.update(new_names)
if len(names) >= 1:
# Hack: Just pick an arbitrary suffix for now. Do something cleverer later.
return names.pop()
else:
return ""
def common_suffix(hosts):
num_components = min(len(h.split(".")) for h in hosts)
longest_suffix = ""
for i in range(1, num_components + 1):
suffixes = set(".".join(h.split(".")[-i:]) for h in hosts)
if len(suffixes) == 1:
longest_suffix = suffixes.pop()
else:
return longest_suffix
return longest_suffix
def extract_names_from_openssl_output(certificates_file):
openssl_output = open(certificates_file, "r").read()
cert = re.findall("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", openssl_output, flags = re.DOTALL)
return extract_names(cert[0])
def supports_starttls(mx_host):
try:
smtpserver = smtplib.SMTP(mx_host, 25, timeout = 2)
smtpserver.ehlo()
smtpserver.starttls()
return True
print "Success: %s" % mx_host
except socket.error as e:
print "Connection to %s failed: %s" % (mx_host, e.strerror)
return False
except smtplib.SMTPException, e:
# In order to talk to some hosts, you need to run this from a host that has a
# reverse DNS entry. AWS instances all have reverse DNS, as an example.
if e[0] == 554:
print e[1]
else:
print "No STARTTLS support on %s" % mx_host, e[0]
return False
def min_tls_version(mail_domain):
protocols = []
for mx_hostname in os.listdir(os.path.join(CERTS_OBSERVED, mail_domain)):
filename = os.path.join(CERTS_OBSERVED, mail_domain, mx_hostname)
contents = open(filename).read()
protocol = re.findall("Protocol : (.*)", contents)[0]
protocols.append(protocol)
return min(protocols)
def collect(mail_domain):
"""
Attempt to connect to each MX hostname for mail_doman and negotiate STARTTLS.
Store the output in a directory with the same name as mail_domain to make
subsequent analysis faster.
"""
print "Checking domain %s" % mail_domain
mkdirp(os.path.join(CERTS_OBSERVED, mail_domain))
answers = dns.resolver.query(mail_domain, 'MX')
for rdata in answers:
mx_host = str(rdata.exchange).rstrip(".")
tls_connect(mx_host, mail_domain)
if __name__ == '__main__':
"""Consume a target list of domains and output a configuration file for those domains."""
if len(sys.argv) < 2:
print("Usage: CheckSTARTTLS.py list-of-domains.txt > output.json")
config = collections.defaultdict(dict)
for input in sys.argv[1:]:
for domain in open(input).readlines():
domain = domain.strip()
suffix = check_certs(domain)
if suffix != "":
min_version = min_tls_version(domain)
suffix_match = "." + suffix
config["acceptable-mxs"][domain] = {
"accept-mx-domains": [suffix_match]
}
config["tls-policies"][suffix_match] = {
"require-tls": True,
"min-tls-version": min_version
}
print json.dumps(config, indent=2, sort_keys=True)

View file

@ -1,34 +0,0 @@
#!/usr/bin/env python
"""
Process Google's TLS delivery data from
https://www.google.com/transparencyreport/saferemail/data/?hl=en
to look for outbound domains that can negotiate an encrypted
connection >99% of the time.
Usage:
./ProcessGoogleSTARTTLSDomains.py google-starttls-domains.csv
"""
import csv
import codecs
import sys
from collections import defaultdict
csvreader = csv.reader(codecs.open(sys.argv[1], "rU", "utf-8"), delimiter=',', quotechar='"')
d = defaultdict(set)
# Google's report doesn't include gmail.com because it's local delivery, but we
# know they support STARTTLS, so manually include them.
d["gmail.com"] = set([1])
for (address_suffix, hostname_suffix, direction, region, region_name, fraction_encrypted) in csvreader:
if direction == "outbound":
# Some domains exist in many TLDs and are summarized as, e.g. yahoo.{...}.
# We're tryingto get a solid list of the relevant TLDs, but in the meantime
# just use .com.
address_suffix = address_suffix.replace("{...}", "com")
try:
d[address_suffix].add(float(fraction_encrypted))
except ValueError:
pass
for address_suffix, fraction_encrypted in d.iteritems():
if min(fraction_encrypted) >= 0.99:
print address_suffix

View file

@ -1,31 +0,0 @@
#!/usr/bin/env bash
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -q -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \
postfix dnsmasq mutt vim
# Provide hostnames so the boxes can talk to each other. DNSMasq will also serve
# results to each box based on these contents.
cat >> /etc/hosts <<EOF
192.168.33.5 sender sender.example.com
192.168.33.7 valid valid-example-recipient.com
EOF
# All local domains get an MX record pointing at themselves
echo selfmx > /etc/dnsmasq.conf
# Not sure why restart is necessary, but otherwise dnsmasq doesn't use
# /etc/hosts to answer queries.
/etc/init.d/dnsmasq restart
if [ "`hostname`" = "sender" ]; then
(while sleep 10; do
echo -e 'Subject: hi\n\nhi' | sendmail vagrant@valid-example-recipient.com
done) &
#ln -sf "/vagrant/postfix-config-sender-tls_policy.cf" /etc/postfix/tls_policy
fi
#ln -sf "/vagrant/postfix-config-`hostname`.cf" /etc/postfix/main.cf
#ln -sf "/vagrant/certificates" /etc/certificates
postfix reload

View file

@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDmzCCAoOgAwIBAgIJAIheS+k2UDobMA0GCSqGSIb3DQEBBQUAMGQxCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDDAKBgNVBAoMA0VGRjEt
MCsGA1UECwwkU1RBUlRUTFMgRXZlcnl3aGVyZSB0ZXN0IGNlcnRpZmljYXRlMB4X
DTE0MDYxMDE4MDg0OVoXDTE5MDYxMDE4MDg1MFowZDELMAkGA1UEBhMCVVMxCzAJ
BgNVBAgMAkNBMQswCQYDVQQHDAJTRjEMMAoGA1UECgwDRUZGMS0wKwYDVQQLDCRT
VEFSVFRMUyBFdmVyeXdoZXJlIHRlc3QgY2VydGlmaWNhdGUwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDDYmUQUjaX6L3n9fNks2yQUFhKUBR1NYenx3w2
DnaZiwpLzI3igJPMQfBdyJGLM1jXZZcpvpgt6yN4OMOLNS2QKBY20gDoQIh0Jmaj
KCoXUbX30H1FfTn+pyU02UpuFsFN3TAk5bQ/BQUYOlMouCowyZ25mnEzzHLeRHKH
Gi2uCH59T53rcgDwjq88pKMVUlndixkOKpeXZkTL++Edg0b5SUpRuzMs6kFmwLoQ
x4xG5lgaAHyu2/9KXhcqielE95s5FNGfi9U2q3nkmpa4dM266DWfkibfvCBP3hiO
Ks+IN+Hyohi2SY7NoDN6hMKcuUNlAK/xD+foA4Ck3R45HII3AgMBAAGjUDBOMB0G
A1UdDgQWBBRPlrBeFMPb27oWESmXykOW1XWLLDAfBgNVHSMEGDAWgBRPlrBeFMPb
27oWESmXykOW1XWLLDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBY
lBQEuRKo927jJFzgvdTKJGIAzxBnC5vg+qKeZkduwxeVEwB13HPP1syvdpAK5dkl
JGuHCF/Yc39HX1OZv7huVRMnyrSKpMt25uqUHirH6Db17HRCS4ZA6rARJfxS6RWu
J31lfXvGRr0hI4yw3XpKGM/c8Gkzji6PsYn4TCPnXjJbRj1GjHFaAuIeyoO0zQjo
OuHnzxs4bIDaV32NHNetuDKSO1GNbenxRiiN1HvQ1vfhzpqerRRaPCHrW4eUcynQ
AAeuC+Ek925t9mH7Ni/kZ0eN7XUwvg3c2lm+LeV+ICP+NWv9r92kfXDZNMhlhNsf
Y8+m1Y9WL3CLj99voNQS
-----END CERTIFICATE-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAw2JlEFI2l+i95/XzZLNskFBYSlAUdTWHp8d8Ng52mYsKS8yN
4oCTzEHwXciRizNY12WXKb6YLesjeDjDizUtkCgWNtIA6ECIdCZmoygqF1G199B9
RX05/qclNNlKbhbBTd0wJOW0PwUFGDpTKLgqMMmduZpxM8xy3kRyhxotrgh+fU+d
63IA8I6vPKSjFVJZ3YsZDiqXl2ZEy/vhHYNG+UlKUbszLOpBZsC6EMeMRuZYGgB8
rtv/Sl4XKonpRPebORTRn4vVNqt55JqWuHTNuug1n5Im37wgT94YjirPiDfh8qIY
tkmOzaAzeoTCnLlDZQCv8Q/n6AOApN0eORyCNwIDAQABAoIBAQCwNXASDSNBU1zZ
8v3kZtDVQjCuLJSWtIU4cnd6RQb/KN9LRxr7GJyyzREbc4SXduJ7uBphQov6daMS
jJcGWBpUdWK7ZB//Vhv6LJvKL7HuP/oNmhEwd2SzXkj25bTznkANmhsOW794Sm2y
0P8orRcX0u0Vc8z+Ozepby+e2qQx29FXznberRv2rXmMeeQqF+sc2MBu34SlcLnK
KVSe7SmDc+DhWJ3XiPoEpiOTbv4EynndXC85owdse/eN/EahFtrfzqm6jDWVga8A
xA/7RD/Urc2L2IsZOB92xOk/tGs5Df8ZrHavzbSo+i0pzYAKvIxr+G2Krl1Tl/Lh
IXwjWPLZAoGBAOsbdxeZUHH6pk497ICIcmW/NDhAY/mSA6vBWZWLcYF7OU/wMzZ6
Wlx9v6oz8tBTzQj/Xkpv0ZLIeQqGTuSQzQ7EcrOiTz4PIpM5y/nf/S0oSXwJ7mJw
Sx8w3XHqHEPCT9G83o7EV7xHuRy8/zQzJ9dkFxznA9sRcO7RhE/tsrMNAoGBANS/
Ql140oyDqyABIlxswyvfJ8Ll4kmb52yNGaCJPSfrAVmSoN15AkbDkpVKFb6hsL6u
xqVPyeVxard4twddrV3GSvTibkGw4fZ8x0FDgDGvCgJ36e4NHb7jQVwwGfNXKMAP
qvYtnE28eAM+zrDHryEhgp4k59zApphr+IU7JQlTAoGAc1h5ODm+rvzTBMX6tyC6
R1Lkcsicg//wDx8ALY9JM8ZZ2u80oQCsPn5vPzjXYwAKMuTexNRRVJtITzKPmDG2
eQ1GXP0/tWnFg8eyXDhZRQNj8hgJPYBsSrQ1oMLD9TZq5LKt2gtYJAZoOkI7Tsfe
Px1a/ZIVYTAQYQqnyHMM3i0CgYAQNirWeJiCwJ3PqIZ3yInu0+hxv5bIySqPaQkk
5JBWdF/79WJwvgHgZpLK8YRKrIONZEAa5MObylK5fGdmFktZs/yOQJrqQpJVeBiu
7nfcUVxP59dZnoI/w419euTfWCrwx8DdVYhtnAkBJk4VxoGf4q/TYTiR59RKFSAw
9trRpQKBgDjFQ9mXJUUKoc61P3PIrSYOlnx89c0sznrwm2eeLLAwr/VQ+vNDGRkC
6kXbVLuTxRaFnLSZTlUd0nSXPbJEM5GII48zbGZ7t/ysPtA9390xk1efTO/ryf/j
pn4FBvpgXaneRs4ExyknLgFfJRUEu17fRbyRPZr0bUlTkKg/ZS5M
-----END RSA PRIVATE KEY-----

View file

@ -1 +0,0 @@
/vagrant/certificates

View file

@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDbTCCAlUCAQEwDQYJKoZIhvcNAQEFBQAwZDELMAkGA1UEBhMCVVMxCzAJBgNV
BAgMAkNBMQswCQYDVQQHDAJTRjEMMAoGA1UECgwDRUZGMS0wKwYDVQQLDCRTVEFS
VFRMUyBFdmVyeXdoZXJlIHRlc3QgY2VydGlmaWNhdGUwHhcNMTQwNjEwMTgxMTQ3
WhcNMTkwNjEwMTgxMTQ3WjCBlDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw
CQYDVQQHDAJTRjEMMAoGA1UECgwDRUZGMTQwMgYDVQQLDCtTVEFSVFRMUyBFdmVy
eXdoZXJlIHRlc3QgY2VydGlmaWNhdGUgKGxlYWYpMScwJQYDVQQDDB5teC52YWxp
ZC1leGFtcGxlLXJlY2lwaWVudC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDZ2NdAwI0LUmXrV7bItKhP8NRzypkYHgApXaoZ6iB6TJnYb1FZyv2Y
wyBdsQQ8zcIgKRyl0rKvNPgHt3riM7nSoB16II6fIQDuR2UnXkhtDOfUd7Ye0wKX
l3A+qoEge42/TfRvzPC6IMa1KH7+dwayprIjLxFUzfMl6GqA/5auLZZtSsV6Pix3
jXZYFPUHPoBJEyNo/bizcdvSZS/7Kwzfc64l7JLA/OGtRbQpcMAOpRRXCx32nt3N
2L1OXz8Q4It+J20oN8Vfin1a7GbAGdktRbz2bV8bmb0ux1NOnddigPUHRRYMIaKN
W7Nrxp2YQpqmsqBmVEVPA903yRc0ZvuRAgMBAAEwDQYJKoZIhvcNAQEFBQADggEB
AMJ3neFM+to/tFhTiAfUnIrQOKyfk+zYN8gC99HD2SUVbu+Cu1qCNJWxbE3bdxxX
y960yX+Or8E4nG9sWbFbzlGEzz0F8DAPEh4UCtb/5MRkhW158Y9LtbheQAQpZolQ
Sma6lnngCmSDr/OpDW34oM8S7+3VOawYv1e1ruCMqizBwilgcsGq2hY1hWca6LMP
X2FHQ+m4mYBV9wJ9WuKMs9uADz9cfMYuA/wMzNeMZyM82w+nSQGZ+mX7YPu+WBJM
k9OcFrUWxrC/uOQfsyqdjLvFjg4/b49jnusn9mZ/KbtLVmkz5P+5kwSn4kcd5Rlw
LLmEGiXiyEjo8UmEVyumrIQ=
-----END CERTIFICATE-----

View file

@ -1,18 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC2jCCAcICAQAwgZQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UE
BwwCU0YxDDAKBgNVBAoMA0VGRjE0MDIGA1UECwwrU1RBUlRUTFMgRXZlcnl3aGVy
ZSB0ZXN0IGNlcnRpZmljYXRlIChsZWFmKTEnMCUGA1UEAwwebXgudmFsaWQtZXhh
bXBsZS1yZWNpcGllbnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEA2djXQMCNC1Jl61e2yLSoT/DUc8qZGB4AKV2qGeogekyZ2G9RWcr9mMMgXbEE
PM3CICkcpdKyrzT4B7d64jO50qAdeiCOnyEA7kdlJ15IbQzn1He2HtMCl5dwPqqB
IHuNv030b8zwuiDGtSh+/ncGsqayIy8RVM3zJehqgP+Wri2WbUrFej4sd412WBT1
Bz6ASRMjaP24s3Hb0mUv+ysM33OuJeySwPzhrUW0KXDADqUUVwsd9p7dzdi9Tl8/
EOCLfidtKDfFX4p9WuxmwBnZLUW89m1fG5m9LsdTTp3XYoD1B0UWDCGijVuza8ad
mEKaprKgZlRFTwPdN8kXNGb7kQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAAhe
pMLNDAaA9fXZ/yqW4ov1pBKYZL1R1RX+YgZqUPoAlrTCiJ0UGIuPcTDGJDMdpZ7x
9gSqwsXrvzXafuHI4GuFjnbY5SIv8zvc+nMXib7IMlyMUcuSBZP8W0sl3ZGWnnpk
legC10c3+I9TfQ7Tl0mpdyf/6yLhM1plxLcIy5bguLJjbBK9JhKNfc84rivqrxUI
+cUqWU13WjMzWdKS6rK5m/Bfleg+jyZ11xYY0QfwNGwuPjfiEBjCs2iqJvdLFRem
FoDq3XBsrH5XohSwWZ6UrZY7wARkmHsYJeYTHulb3MkDPCQKlbU+2SMIpbNk43+/
fN/ctxWXg3vd/q6eJiE=
-----END CERTIFICATE REQUEST-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA2djXQMCNC1Jl61e2yLSoT/DUc8qZGB4AKV2qGeogekyZ2G9R
Wcr9mMMgXbEEPM3CICkcpdKyrzT4B7d64jO50qAdeiCOnyEA7kdlJ15IbQzn1He2
HtMCl5dwPqqBIHuNv030b8zwuiDGtSh+/ncGsqayIy8RVM3zJehqgP+Wri2WbUrF
ej4sd412WBT1Bz6ASRMjaP24s3Hb0mUv+ysM33OuJeySwPzhrUW0KXDADqUUVwsd
9p7dzdi9Tl8/EOCLfidtKDfFX4p9WuxmwBnZLUW89m1fG5m9LsdTTp3XYoD1B0UW
DCGijVuza8admEKaprKgZlRFTwPdN8kXNGb7kQIDAQABAoIBAHAZgUq0yt+UmxWr
oUdOj33zc5/SFU2vwm2G4U1MiUHlwRT602Xdavn9Dt6nhIK1bruV7EP4VDKMk0WF
SRq1e13DPuflcP65wPzciFTl02cqSPGwWGssMh1HtF7K5n+MlLhoqOwPDaD51MbL
++192lh8Jxar1cNJ52EOZB/VZfhiUZ88JAITWam6clId8KS4cy7SvQ9haptcPlfl
wIeCti2eI4nvC0IR0SCEqWAax6XqypA5k2fjQJklcnBK1R+H1muc00J5lMIz4U8q
qFb5RgxIDMcEeQZWmzfBjU5hO0q/51QyWFFFuBL00MkT4djCHjO/IPYc6DQoAoZR
TbMCWwECgYEA+i3W44tSY96Fdn1gDyYhDor9GPN0XwmRBHpl44TxBLG48I6cGLYI
l0InEh7pu7/0fw+cWylx2OLrl2HeGlWilEOpP2ucJcswcNibATtgSdnu7PjC4eAW
46a5+2hbFk/NONU5VTGClJHCvKzYPWMrAIkRT//WdkIVs23i7bRcOjkCgYEA3upr
cnMhnwVmZrGAIm+DtiDJCc4uVXeqLD2j9csCVNCw/CQAjJ3/5VlQDSjzerjijw/V
uoA+Su8xMoMCg6mAihqdxBMrbwdlh7vje23RaxbeQkIY5JIVAsdDr+UtwofaX3Zx
j3RW5jNeouVlvtExyKZZhyMwuh8d59m4V65/rBkCgYBH+f40EvZOQ0v0jhef5CFo
lLZCgnB9kzwEpM5BihLpfdQuaWkhduW71s102i321UAbejtKwv69HnQXZpHG09Jl
g53i4CvZd77lCHx3+0Q1mxyxUtSGtbkAIAyr9xcVsTni2v2WtBrUcacsLzI7XxeV
HNo9QObLuTGTIM9EAjryiQKBgQCIZEBX56/jn6c3IFX5O+gH8OlxEXFyI+TAavq+
Mnd7s7EGpXSclTP0fYAofSz0otkklZi9Iyh6Kv4cHOLV8klOtthfFyeVKJ5rvX+D
jv76miRlwBGBEQzABXIZ1oz4IK1xiYQUNSfSdA3sd5WYemEOlxHiSJrQ1qcyrBlJ
tOAzSQKBgQDjLQIZ0rmSAMUCGi7WlDle/pnf6sUEFUPWUqzHOJbJk/CZIIf1IT6o
Evep9eeP6QjiupusY/uSlgjjjj7yxZU5q3VV/0cQl6QhwxG9wqpa8x32wwaM1WBZ
rJgsuSjcEoluAqfX/bqRtumNtQ213DSADzSCpOetlJb+ARANk7YULg==
-----END RSA PRIVATE KEY-----

View file

@ -1 +0,0 @@
valid-example-recipient.com secure match=valid-example-recipient.com:.valid-example-recipient.com

View file

@ -1,39 +0,0 @@
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
# Debian specific: Specifying a file name will cause the first
# line of that file to be used as the name. The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
# appending .domain is the MUA's job.
append_dot_mydomain = no
# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h
readme_directory = no
# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.
myhostname = sender.example.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = sender.example.com, localhost.example.com, , localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all

View file

@ -1,46 +0,0 @@
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
# Debian specific: Specifying a file name will cause the first
# line of that file to be used as the name. The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
# appending .domain is the MUA's job.
append_dot_mydomain = no
# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h
readme_directory = no
# TLS parameters
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.
myhostname = valid-example-recipient.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = valid-example-recipient.com, localhost.example.com, , localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
# STARTLS Everywhere recommended best-practice settings
smtpd_tls_session_cache_timeout = 3600s
smtpd_tls_received_header = yes
#STARTTLS EVERYWHERE MAGIC STARTS HERE
smtp_tls_policy_maps = texthash:/etc/postfix/tls_policy
smtpd_tls_cert_file=/etc/certificates/valid.crt
smtpd_tls_key_file=/etc/certificates/valid.key