Processing of forwarded messages

This commit is contained in:
Dominik Chilla 2021-01-07 10:58:15 +01:00
parent d01c98333e
commit 8326967af3
7 changed files with 137 additions and 45 deletions

View File

@ -1,3 +1,3 @@
{
"python.pythonPath": "/home/dominik/src/git/ExOTA-Milter/venv/bin/python3"
"python.pythonPath": "/home/dominik/src/github/ExOTA-Milter/venv/bin/python3"
}

View File

@ -47,6 +47,7 @@ services:
MILTER_TRUSTED_AUTHSERVID: 'my-auth-serv-id'
MILTER_X509_ENABLED: 'some_value'
MILTER_X509_TRUSTED_CN: 'mail.protection.outlook.com'
MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
MILTER_ADD_HEADER: 'some_value'
MILTER_AUTHSERVID: 'my-auth-serv-id'
volumes:

View File

@ -8,7 +8,7 @@ start
note left: From, Authentication-Results, X-MS-Exchange-CrossTenant-Id
:HDR: Recognising sender domain;
note left: Taken from RFC5322 From-header. RFC5321.mail (envelope) is NOT relevant!
note left: Taken from RFC5322.From header and/or RFC5322.Resent-From header. RFC5321.mail (envelope) is NOT relevant!
:EOM: Looking up policy in backend;
note left: Based on RFC5322.from domain

View File

@ -37,6 +37,8 @@ g_milter_policy_file = '/data/policy.json'
g_milter_x509_enabled = False
# ENV[MILTER_X509_TRUSTED_CN]
g_milter_x509_trusted_cn = 'mail.protection.outlook.com'
# ENV[MILTER_X509_IP_WHITELIST]
g_milter_x509_ip_whitelist = ['127.0.0.1','::1']
# ENV[MILTER_ADD_HEADER]
g_milter_add_header = False
# ENV[MILTER_AUTHSERVID]
@ -57,9 +59,15 @@ class ExOTAMilter(Milter.Base):
self.conn_reused = False
self.hdr_from = None
self.hdr_from_domain = None
self.hdr_resent_from = None
self.hdr_resent_from_domain = None
self.forwarded = False
self.hdr_tenant_id = None
self.hdr_tenant_id_count = 0
self.x509_whitelisted = False
self.dkim_valid = False
self.passed_dkim_results = []
self.dkim_aligned = False
self.xar_hdr_count = 0
# https://stackoverflow.com/a/2257449
self.mconn_id = g_milter_name + ': ' + ''.join(
@ -76,7 +84,7 @@ class ExOTAMilter(Milter.Base):
if 'reason' in kwargs:
message = "{0} - reason: {1}".format(message, kwargs['reason'])
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
": milter_action=reject"
": milter_action=reject message={0}".format(message)
)
self.setreply('550','5.7.1', message)
return Milter.REJECT
@ -138,6 +146,23 @@ class ExOTAMilter(Milter.Base):
self.hdr_from, self.hdr_from_domain
)
)
# Parse RFC-5322-Resent-From header (Forwarded)
if(name.lower() == "Resent-From".lower()):
hdr_5322_resent_from = email.utils.parseaddr(hval)
self.hdr_resent_from = hdr_5322_resent_from[1].lower()
m = re.match(g_re_domain, self.hdr_resent_from)
if m is None:
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
"Could not determine domain-part of 5322.resent_from=" + self.hdr_resent_from
)
else:
self.hdr_resent_from_domain = m.group(1).lower()
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: 5322.resentfrom={0}, 5322.resent_from_domain={1}".format(
self.hdr_resent_from, self.hdr_resent_from_domain
)
)
# Parse non-standardized X-MS-Exchange-CrossTenant-Id header
elif(name.lower() == "X-MS-Exchange-CrossTenant-Id".lower()):
@ -159,6 +184,12 @@ class ExOTAMilter(Milter.Base):
for ar_result in ar.results:
if ar_result.method == 'dkim':
if ar_result.result == 'pass':
self.passed_dkim_results.append({
"sdid": ar_result.header_d
})
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: DKIM passed SDID {0}".format(ar_result.header_d)
)
self.dkim_valid = True
else:
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
@ -183,33 +214,42 @@ class ExOTAMilter(Milter.Base):
# Check if client certificate CN matches trusted CN
if g_milter_x509_enabled:
cert_subject = self.getsymval('{cert_subject}')
if cert_subject is None:
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
+ "/EOM: No trusted x509 client CN found - action=reject"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'No trusted x509 client CN found'
)
else:
if g_milter_x509_trusted_cn.lower() == cert_subject.lower():
self.x509_client_valid = True
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Trusted x509 client CN {0}".format(cert_subject)
for whitelisted_client_ip in g_milter_x509_ip_whitelist:
if self.client_ip == whitelisted_client_ip:
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
+ "/EOM: x509 CN check: client-IP '{0}' is whitelisted".format(
whitelisted_client_ip
)
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM Untrusted x509 client CN {0} - action=reject".format(cert_subject)
self.x509_whitelisted = True
if not self.x509_whitelisted:
cert_subject = self.getsymval('{cert_subject}')
if cert_subject is None:
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
+ "/EOM: No trusted x509 client CN found - action=reject"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = "Untrusted x509 client CN: {0}".format(cert_subject)
reason = 'No trusted x509 client CN found'
)
else:
if g_milter_x509_trusted_cn.lower() == cert_subject.lower():
self.x509_client_valid = True
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Trusted x509 client CN {0}".format(cert_subject)
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Untrusted x509 client CN {0} - action=reject".format(cert_subject)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = "Untrusted x509 client CN: {0}".format(cert_subject)
)
if self.hdr_from is None:
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM exception: could not determine 5322.from header - action=reject"
"/EOM: exception: could not determine 5322.from header - action=reject"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
@ -221,20 +261,48 @@ class ExOTAMilter(Milter.Base):
try:
policy = g_policy_backend.get(self.hdr_from_domain)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM Policy for 5322.from_domain={0} fetched from backend".format(self.hdr_from_domain)
"/EOM: Policy for 5322.from_domain={0} fetched from backend".format(
self.hdr_from_domain
)
)
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM {0}".format(e.message)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = "No policy for {0}".format(self.hdr_from_domain)
"/EOM: 5322.from: {0}".format(e.message)
)
# Forwarded message? Maybe the Resent-From header domain matches.
if self.hdr_resent_from_domain is not None:
try:
policy = g_policy_backend.get(self.hdr_resent_from_domain)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy for 5322.resent_from_domain={0} fetched from backend".format(
self.hdr_resent_from_domain
)
)
self.forwarded = True
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Forwarded message -> Policy for 5322.resent_from_domain={0} found.".format(
self.hdr_resent_from_domain
)
)
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: 5322.resent-from: {0}".format(e.message)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = "No policy for 5322.resent_from_domain {0}".format(
self.hdr_resent_from_domain
)
)
else:
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = "No policy for 5322.from_domain {0}".format(self.hdr_from_domain)
)
if self.hdr_tenant_id is None:
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
"/EOM: exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
@ -268,15 +336,23 @@ class ExOTAMilter(Milter.Base):
)
if self.dkim_valid:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Found valid DKIM authentication result for 5322.from_domain={0}".format(
self.hdr_from_domain
)
"/EOM: Valid DKIM signatures found"
)
for passed_dkim_result in self.passed_dkim_results:
if self.hdr_from_domain == passed_dkim_result['sdid']:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Found aligned DKIM signature for SDID: {0}".format(
passed_dkim_result['sdid']
)
)
self.dkim_aligned = True
if not self.dkim_aligned:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No aligned DKIM signatures found!"
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No valid DKIM authentication result found for 5322.from_domain={0}".format(
self.hdr_from_domain
)
"/EOM: No valid DKIM authentication result found"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
@ -297,12 +373,16 @@ class ExOTAMilter(Milter.Base):
if g_milter_add_header:
try:
self.addheader("X-ExOTA-Authentication-Results",
"{0};\n auth=pass header.d={1} dkim={2} x509_client_trust={3}".format(
g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(),
g_milter_x509_enabled
)
addhdr_value = str(
"{0};\n" +
" auth=pass 5322_from_domain={1} dkim={2} dkim_aligned={3} " +
"x509_client_trust={4} forwarded={5}"
).format(
g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(),
self.dkim_aligned, g_milter_x509_enabled, self.forwarded
)
logging.debug(addhdr_value)
self.addheader("X-ExOTA-Authentication-Results", addhdr_value)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: AR-header added"
)
@ -313,8 +393,8 @@ class ExOTAMilter(Milter.Base):
if g_milter_dkim_enabled:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authorized (dkim_enabled={0})".format(
str(policy.is_dkim_enabled())
"/EOM: Tenant successfully authorized (dkim_enabled={0} dkim_aligned={1})".format(
policy.is_dkim_enabled(), self.dkim_aligned
)
)
else:
@ -375,6 +455,10 @@ if __name__ == "__main__":
if 'MILTER_X509_TRUSTED_CN' in os.environ:
g_milter_x509_trusted_cn = os.environ['MILTER_X509_TRUSTED_CN']
logging.info("ENV[MILTER_X509_TRUSTED_CN]: {0}".format(g_milter_x509_trusted_cn))
if 'MILTER_X509_IP_WHITELIST' in os.environ:
g_milter_x509_ip_whitelist = "".join(os.environ['MILTER_X509_IP_WHITELIST'].split())
g_milter_x509_ip_whitelist = g_milter_x509_ip_whitelist.split(',')
logging.info("ENV[MILTER_X509_IP_WHITELIST]: {0}".format(g_milter_x509_ip_whitelist))
logging.info("ENV[MILTER_X509_ENABLED]: {0}".format(g_milter_x509_enabled))
if 'MILTER_POLICY_SOURCE' in os.environ:
g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE']

View File

@ -89,7 +89,7 @@ class ExOTAPolicyBackendJSON(ExOTAPolicyBackend):
return ExOTAPolicy(self.policies[from_domain])
except KeyError as e:
raise ExOTAPolicyNotFoundException(
"Policy for from_domain={0} not found".format(from_domain)
"Policy for domain={0} not found".format(from_domain)
) from e
except Exception as e:
raise ExOTAPolicyException(

View File

@ -19,6 +19,7 @@ export MILTER_DKIM_ENABLED=yepp
export MILTER_TRUSTED_AUTHSERVID=my-auth-serv-id
export MILTER_X509_ENABLED=yepp
export MILTER_X509_TRUSTED_CN=mail.protection.outlook.com
export MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
export MILTER_ADD_HEADER=yepp
export MILTER_AUTHSERVID=my-auth-serv-id
```

View File

@ -6,6 +6,9 @@ conn = mt.connect(socket)
if conn == nil then
error "mt.connect() failed"
end
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(3)
@ -18,7 +21,7 @@ if mt.getreply(conn) ~= SMFIR_CONTINUE then
end
-- 5321.RCPT+MACROS
mt.macro(conn, SMFIC_RCPT, '{client_addr}', "127.128.129.130", "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.com")
mt.macro(conn, SMFIC_RCPT, "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= nil then
error "mt.rcptto() failed"
end
@ -30,6 +33,9 @@ end
if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@yad.onmicrosoft.com>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "aaa-resent-fRoM", '"Blah Blubb" <blah@yad.onmicrosoft.COMa>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "x-mS-EXCHANGE-crosstenant-id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then
error "mt.header(Subject) failed"
end
@ -45,7 +51,7 @@ end
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.com header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.comx header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n dkim=fail header.d=yad.onmicrosoft.com header.s=selector2-asdf header.b=mmmjFpv8") ~= nil then