dmarc: added relaxed alignment tests

This commit is contained in:
Matt Simerson 2013-04-21 02:49:39 -04:00
parent 610c39dc74
commit 091843927d

View File

@ -40,16 +40,12 @@ See Section 10 of the draft: Domain Owner Actions
pct=50; (percent of messages to filter) pct=50; (percent of messages to filter)
=head2
=head1 DRAFT =head1 DRAFT
http://www.dmarc.org/draft-dmarc-base-00-02.txt http://www.dmarc.org/draft-dmarc-base-00-02.txt
=head1 TODO =head1 TODO
1. run dmarc before SPF, if DMARC policy is discovered, ignore SPF
2. provide dmarc feedback to domains that request it 2. provide dmarc feedback to domains that request it
3. If a message has multiple 'From' recipients, reject it 3. If a message has multiple 'From' recipients, reject it
@ -58,7 +54,7 @@ http://www.dmarc.org/draft-dmarc-base-00-02.txt
=head1 IMPLEMENTATION =head1 IMPLEMENTATION
1. Primary identifier is RFC5322.From field 1. Primary identifier is RFC5322.From field (From: header)
2. Senders can specify strict or relaxed mode 2. Senders can specify strict or relaxed mode
@ -72,29 +68,6 @@ http://www.dmarc.org/draft-dmarc-base-00-02.txt
RFC5322.From purports to be from a domain that appears to be RFC5322.From purports to be from a domain that appears to be
either non-existent or incapable of receiving mail. either non-existent or incapable of receiving mail.
=head2 Reports should include
The report SHOULD include the following data:
o Enough information for the report consumer to re-calculate DMARC
disposition based on the published policy, message dispositon, and
SPF, DKIM, and identifier alignment results. {R12}
o Data for each sender subdomain separately from mail from the
sender's organizational domain, even if no subdomain policy is
applied. {R13}
o Sending and receiving domains {R17}
o The policy requested by the Domain Owner and the policy actually
applied (if different) {R18}
o The number of successful authentications {R19}
o The counts of messages based on all messages received even if
their delivery is ultimately blocked by other filtering agents
{R20}
=cut =cut
use strict; use strict;
@ -123,15 +96,13 @@ sub data_post_handler {
# 11.1. Extract Author Domain # 11.1. Extract Author Domain
# TODO: check exists_in_dns result, and possibly reject here if domain non-exist
my $from_host = $self->get_from_host($transaction) or return DECLINED; my $from_host = $self->get_from_host($transaction) or return DECLINED;
if (!$self->exists_in_dns($from_host)) { my $org_host = $self->get_organizational_domain($from_host);
my $org_host = $self->get_organizational_domain($from_host);
if (!$self->exists_in_dns($org_host)) {
$self->log(LOGINFO, "fail, domain/org not in DNS");
#return $self->get_reject(); if (!$self->exists_in_dns($from_host)) {
return DECLINED; if (!$self->exists_in_dns($org_host)) {
$self->log(LOGINFO, "fail, $from_host not in DNS");
return $self->get_reject("RFC5322.From host does not exist");
} }
} }
@ -140,18 +111,30 @@ sub data_post_handler {
or return DECLINED; or return DECLINED;
# 3. Perform DKIM signature verification checks. A single email may # 3. Perform DKIM signature verification checks. A single email may
# contain multiple DKIM signatures. The results of this step are # contain multiple DKIM signatures. The results MUST include the
# passed to the remainder of the algorithm and MUST include the # value of the "d=" tag from all DKIM signatures that validated.
# value of the "d=" tag from all DKIM signatures that successfully #my $dkim_sigs = $self->connection->notes('dkim_pass_domains') || [];
# validated.
my $dkim_sigs = $self->connection->notes('dkim_pass_domains') || [];
# 4. Perform SPF validation checks. The results of this step are # 4. Perform SPF validation checks. The results of this step
# passed to the remainder of the algorithm and MUST include the # MUST include the domain name from the RFC5321.MailFrom if SPF
# domain name from the RFC5321.MailFrom if SPF evaluation returned # evaluation returned a "pass" result.
# a "pass" result.
my $spf_dom = $transaction->notes('spf_pass_host'); my $spf_dom = $transaction->notes('spf_pass_host');
# 5. Conduct identifier alignment checks.
return DECLINED
if $self->is_aligned($from_host, $org_host, $policy, $spf_dom );
# 6. Apply policy. Emails that fail the DMARC mechanism check are
# disposed of in accordance with the discovered DMARC policy of the
# Domain Owner. See Section 6.2 for details.
return DECLINED if lc $policy->{p} eq 'none';
return $self->get_reject("failed DMARC policy");
}
sub is_aligned {
my ($self, $from_host, $org_host, $policy, $spf_dom) = @_;
# 5. Conduct identifier alignment checks. With authentication checks # 5. Conduct identifier alignment checks. With authentication checks
# and policy discovery performed, the Mail Receiver checks if # and policy discovery performed, the Mail Receiver checks if
# Authenticated Identifiers fall into alignment as decribed in # Authenticated Identifiers fall into alignment as decribed in
@ -160,34 +143,43 @@ sub data_post_handler {
# the DMARC mechanism check. All other conditions (authentication # the DMARC mechanism check. All other conditions (authentication
# failures, identifier mismatches) are considered to be DMARC # failures, identifier mismatches) are considered to be DMARC
# mechanism check failures. # mechanism check failures.
my $dkim_sigs = $self->connection->notes('dkim_pass_domains') || [];
foreach (@$dkim_sigs) { foreach (@$dkim_sigs) {
if ($_ eq $from_host) { # strict alignment if ($_ eq $from_host) { # strict alignment
$self->log(LOGINFO, "pass, DKIM alignment"); $self->log(LOGINFO, "pass, DKIM aligned");
$self->adjust_karma(2); # big karma boost $self->adjust_karma(1);
return DECLINED; return 1;
} }
next if $policy->{adkim} && lc $policy->{adkim} eq 's'; # strict pol.
# default policy is relaxed
if ( $_ eq $org_host ) {
$self->log(LOGINFO, "pass, DKIM aligned, relaxed");
$self->adjust_karma(1);
return 1;
};
} }
if ($spf_dom && $spf_dom eq $from_host) { return 0 if ! $spf_dom;
$self->adjust_karma(2); # big karma boost if ($spf_dom eq $from_host) {
$self->log(LOGINFO, "pass, SPF alignment"); $self->adjust_karma(1);
return DECLINED; $self->log(LOGINFO, "pass, SPF aligned");
return 1;
}
return 0 if ($policy->{aspf} && lc $policy->{aspf} eq 's' ); # strict pol
if ($spf_dom eq $org_host) {
$self->adjust_karma(1);
$self->log(LOGINFO, "pass, SPF aligned, relaxed");
return 1;
} }
# 6. Apply policy. Emails that fail the DMARC mechanism check are return 0;
# disposed of in accordance with the discovered DMARC policy of the };
# Domain Owner. See Section 6.2 for details.
$self->log(LOGINFO, "skip, NEED RELAXED alignment");
return DECLINED;
}
sub discover_policy { sub discover_policy {
my ($self, $from_host) = @_; my ($self, $from_host) = @_;
# 1. Mail Receivers MUST query the DNS for a DMARC TXT record at the # 1. Mail Receivers MUST query the DNS for a DMARC TXT record...
# DNS domain matching the one found in the RFC5322.From domain in
# the message. A possibly empty set of records is returned.
my @matches = $self->fetch_dmarc_record($from_host); # 2. within my @matches = $self->fetch_dmarc_record($from_host); # 2. within
if (0 == scalar @matches) { if (0 == scalar @matches) {
@ -304,29 +296,45 @@ sub get_organizational_domain {
sub exists_in_dns { sub exists_in_dns {
my ($self, $domain) = @_; my ($self, $domain) = @_;
# the DMARC draft suggests rejecting messages whose From: domain does not
# exist in DNS. That's as far as it goes. So I went back to the ADSP (from
# where DMARC this originated, which in turn led me to the ietf-dkim email
# list where a handful of 'experts' failed to agree on The Right Way to
# perform this test. And thus no direction was given.
# As they point out:
# MX records aren't mandatory.
# A or AAAA records as fallback aren't reliable either.
# I chose to query the name and match NS,MX,A,or AAAA records. Since it gets
# repeated for the for the Organizational Name, if it fails, there's no
# delegation from the TLD.
my $res = $self->init_resolver(); my $res = $self->init_resolver();
my $query = $res->send($domain, 'NS') or do { my $query = $res->send($domain) or do {
if ($res->errorstring eq 'NXDOMAIN') { if ($res->errorstring eq 'NXDOMAIN') {
$self->log(LOGDEBUG, "fail, non-existent domain: $domain"); $self->log(LOGDEBUG, "fail, non-existent domain: $domain");
return; return;
} }
$self->log(LOGINFO, $self->log(LOGINFO, "error, looking up $domain: " . $res->errorstring);
"error, looking up NS for $domain: " . $res->errorstring);
return; return;
}; };
my @matches; my @matches;
for my $rr ($query->answer) { for my $rr ($query->answer) {
next if $rr->type ne 'NS'; next if $rr->type !~ /(?:NS|MX|A|AAAA)/;
push @matches, $rr->nsdname; push @matches, $rr->nsdname;
} }
if (0 == scalar @matches) { if (0 == scalar @matches) {
$self->log(LOGDEBUG, "fail, zero NS for $domain"); $self->log(LOGDEBUG, "fail, no records for $domain");
} }
return @matches; return @matches;
} }
sub fetch_dmarc_record { sub fetch_dmarc_record {
my ($self, $zone) = @_; my ($self, $zone) = @_;
# 1. Mail Receivers MUST query the DNS for a DMARC TXT record at the
# DNS domain matching the one found in the RFC5322.From domain in
# the message. A possibly empty set of records is returned.
my $res = $self->init_resolver(); my $res = $self->init_resolver();
my $query = $res->send('_dmarc.' . $zone, 'TXT'); my $query = $res->send('_dmarc.' . $zone, 'TXT');
my @matches; my @matches;
@ -366,6 +374,34 @@ sub parse_policy {
return %dmarc; return %dmarc;
} }
sub external_report {
=pod
The report SHOULD include the following data:
o Enough information for the report consumer to re-calculate DMARC
disposition based on the published policy, message dispositon, and
SPF, DKIM, and identifier alignment results. {R12}
o Data for each sender subdomain separately from mail from the
sender's organizational domain, even if no subdomain policy is
applied. {R13}
o Sending and receiving domains {R17}
o The policy requested by the Domain Owner and the policy actually
applied (if different) {R18}
o The number of successful authentications {R19}
o The counts of messages based on all messages received even if
their delivery is ultimately blocked by other filtering agents {R20}
=cut
};
sub verify_external_reporting { sub verify_external_reporting {
=head2 Verify External Destinations =head2 Verify External Destinations