added Authentication-Results header, with provider

dkim, dmarc, fcrdns (iprev), spf, and smtp-auth
This commit is contained in:
Matt Simerson 2013-05-01 00:35:49 -07:00
parent b1afbabf4c
commit 4ae16219bd
6 changed files with 123 additions and 29 deletions

View File

@ -271,6 +271,34 @@ sub store_deferred_reject {
return (DECLINED); return (DECLINED);
} }
sub store_auth_results {
my ($self, $value) = @_;
my @headers = $self->transaction->header->get('Authentication-Results');
chomp @headers;
my @deleteme;
for ( my $i = 0; $i < scalar @headers; $i++ ) {
my @values = split /;/, $headers[$i];
if ( $self->config->('me') ne $values[0] ) { # some other MTA
# we generally want to remove Authentication-Results headers added by other
# MTAs (so our downstream can trust the A-R header we insert), but we also
# don't want to invalidate DKIM signatures.
# TODO: parse the DKIM signature(s) to see if A-R header is signed
if ( ! $self->transaction->header->get('DKIM-Signature') ) {
$self->log(LOGINFO, "deleted auth-results from $_");
push @deleteme, $i;
};
next;
};
push @values, $value;
$self->log(LOGINFO, "appended to auth-results: $value");
$self->transaction->header->replace('Authentication->Results', join('; ', @values ), $i);
}
foreach ( @deleteme ) {
$self->transaction->header->delete('Authentication-Results', $_);
};
};
sub init_resolver { sub init_resolver {
my $self = shift; my $self = shift;
my $timeout = $self->{_args}{dns_timeout} || shift || 5; my $timeout = $self->{_args}{dns_timeout} || shift || 5;

View File

@ -770,6 +770,7 @@ sub data_respond {
my $esmtp = substr($smtp, 0, 1) eq "E"; my $esmtp = substr($smtp, 0, 1) eq "E";
my $authheader = ''; my $authheader = '';
my $sslheader = ''; my $sslheader = '';
my $auth_result = 'none';
if (defined $self->connection->notes('tls_enabled') if (defined $self->connection->notes('tls_enabled')
and $self->connection->notes('tls_enabled')) and $self->connection->notes('tls_enabled'))
@ -780,15 +781,28 @@ sub data_respond {
. " encrypted) "; . " encrypted) ";
} }
if (defined $self->{_auth} and $self->{_auth} == OK) { if (defined $self->{_auth} ) {
$smtp .= "A" if $esmtp; # RFC3848 my $mech = $self->{_auth_mechanism};
$authheader = my $user = $self->{_auth_user};
"(smtp-auth username $self->{_auth_user}, mechanism $self->{_auth_mechanism})\n"; $auth_result = "auth=";
if ( $self->{_auth} == OK) {
$smtp .= "A" if $esmtp; # RFC3848
$authheader = "(smtp-auth username $user, mechanism $mech)\n";
$auth_result .= 'pass';
}
else {
$auth_result .= 'fail';
};
$auth_result .= " ($mech) smtp.auth=$user";
} }
$header->add("Received", $header->add('Received',
$self->received_line($smtp, $authheader, $sslheader), 0); $self->received_line($smtp, $authheader, $sslheader), 0);
# RFC 5451: used in AUTH, DKIM, DOMAINKEYS, SENDERID, SPF
$header->add('Authentication-Results',
join('; ', $self->config('me'), $auth_result ) );
# if we get here without seeing a terminator, the connection is # if we get here without seeing a terminator, the connection is
# probably dead. # probably dead.
unless ($complete) { unless ($complete) {

View File

@ -222,6 +222,9 @@ sub validate_it {
my $result = $dkim->result; my $result = $dkim->result;
my $mess = $self->get_details($dkim); my $mess = $self->get_details($dkim);
$self->store_auth_results("dkim=" .$dkim->result_detail . " header.i=@".$dkim->signature->domain);
#$self->add_header($mess);
foreach my $t (qw/ pass fail invalid temperror none /) { foreach my $t (qw/ pass fail invalid temperror none /) {
next if $t ne $result; next if $t ne $result;
my $handler = 'handle_sig_' . $t; my $handler = 'handle_sig_' . $t;
@ -286,8 +289,7 @@ sub handle_sig_fail {
my ($self, $dkim, $mess) = @_; my ($self, $dkim, $mess) = @_;
$self->adjust_karma(-1); $self->adjust_karma(-1);
return return $self->get_reject("signature invalid: " . $dkim->result_detail,
$self->get_reject("DKIM signature invalid: " . $dkim->result_detail,
$mess); $mess);
} }
@ -316,12 +318,10 @@ sub handle_sig_invalid {
$self->log(LOGINFO, $mess); $self->log(LOGINFO, $mess);
if ($prs->{accept}) { if ($prs->{accept}) {
$self->add_header($mess);
$self->log(LOGERROR, "error, invalid signature but accept policy!?"); $self->log(LOGERROR, "error, invalid signature but accept policy!?");
return DECLINED; return DECLINED;
} }
elsif ($prs->{neutral}) { elsif ($prs->{neutral}) {
$self->add_header($mess);
$self->log(LOGERROR, "error, invalid signature but neutral policy?!"); $self->log(LOGERROR, "error, invalid signature but neutral policy?!");
return DECLINED; return DECLINED;
} }
@ -333,7 +333,6 @@ sub handle_sig_invalid {
# this should never happen # this should never happen
$self->log(LOGINFO, "error, invalid signature, unhandled"); $self->log(LOGINFO, "error, invalid signature, unhandled");
$self->add_header($mess);
return DECLINED; return DECLINED;
} }
@ -527,8 +526,6 @@ sub get_selector {
sub add_header { sub add_header {
my $self = shift; my $self = shift;
my $header = shift or return; my $header = shift or return;
# consider adding Authentication-Results header, (RFC 5451)
$self->qp->transaction->header->add('X-DKIM-Authentication', $header, 0); $self->qp->transaction->header->add('X-DKIM-Authentication', $header, 0);
} }

View File

@ -122,24 +122,31 @@ sub data_post_handler {
# evaluation returned a "pass" result. # evaluation returned a "pass" result.
my $spf_dom = $transaction->notes('spf_pass_host'); my $spf_dom = $transaction->notes('spf_pass_host');
my $effective_policy = ( $self->{_args}{is_subdomain} && defined $policy->{sp} )
? $policy->{sp} : $policy->{p};
# 5. Conduct identifier alignment checks. # 5. Conduct identifier alignment checks.
return DECLINED if ( $self->is_aligned($from_dom, $org_dom, $policy, $spf_dom ) ) {
if $self->is_aligned($from_dom, $org_dom, $policy, $spf_dom ); $self->store_auth_results("dmarc=pass (p=$effective_policy) d=$from_dom");
return DECLINED;
};
# 6. Apply policy. Emails that fail the DMARC mechanism check are # 6. Apply policy. Emails that fail the DMARC mechanism check are
# disposed of in accordance with the discovered DMARC policy of the # disposed of in accordance with the discovered DMARC policy of the
# Domain Owner. See Section 6.2 for details. # Domain Owner. See Section 6.2 for details.
if ( $self->{_args}{is_subdomain} && defined $policy->{sp} ) { if ( lc $effective_policy eq 'none' ) {
return DECLINED if lc $policy->{sp} eq 'none'; $self->store_auth_results("dmarc=fail (p=none) d=$from_dom");
return DECLINED;
}; };
return DECLINED if lc $policy->{p} eq 'none';
my $pct = $policy->{pct} || 100; my $pct = $policy->{pct} || 100;
if ( $pct != 100 && int(rand(100)) >= $pct ) { if ( $pct != 100 && int(rand(100)) >= $pct ) {
$self->log("fail, tolerated, policy, sampled out"); $self->log("fail, tolerated, policy, sampled out");
$self->store_auth_results("dmarc=sampled_out (p=$effective_policy) d=$from_dom");
return DECLINED; return DECLINED;
}; };
$self->store_auth_results("dmarc=fail (p=$effective_policy) d=$from_dom");
return $self->get_reject("failed DMARC policy"); return $self->get_reject("failed DMARC policy");
} }

View File

@ -6,7 +6,7 @@ Forward Confirmed RDNS - http://en.wikipedia.org/wiki/FCrDNS
=head1 DESCRIPTION =head1 DESCRIPTION
Determine if the SMTP sender has matching forward and reverse DNS. Determine if the SMTP sender has matching forward and reverse DNS.
Sets the connection note fcrdns. Sets the connection note fcrdns.
@ -88,6 +88,38 @@ From Wikipedia summary:
3. Any A or AAAA record returned by the second query is then compared against the original IP address (check_ip_match), and if there is a match, then the FCrDNS check passes. 3. Any A or AAAA record returned by the second query is then compared against the original IP address (check_ip_match), and if there is a match, then the FCrDNS check passes.
=head1 iprev
# https://www.ietf.org/rfc/rfc5451.txt
2.4.3. "iprev" Results
The result values are used by the "iprev" method, defined in
Section 3, are as follows:
pass: The DNS evaluation succeeded, i.e., the "reverse" and
"forward" lookup results were returned and were in agreement.
fail: The DNS evaluation failed. In particular, the "reverse" and
"forward" lookups each produced results but they were not in
agreement, or the "forward" query completed but produced no
result, e.g., a DNS RCODE of 3, commonly known as NXDOMAIN, or an
RCODE of 0 (NOERROR) in a reply containing no answers, was
returned.
temperror: The DNS evaluation could not be completed due to some
error that is likely transient in nature, such as a temporary DNS
error, e.g., a DNS RCODE of 2, commonly known as SERVFAIL, or
other error condition resulted. A later attempt may produce a
final result.
permerror: The DNS evaluation could not be completed because no PTR
data are published for the connecting IP address, e.g., a DNS
RCODE of 3, commonly known as NXDOMAIN, or an RCODE of 0 (NOERROR)
in a reply containing no answers, was returned. This prevented
completion of the evaluation.
=cut
=head1 AUTHOR =head1 AUTHOR
@ -136,9 +168,8 @@ sub connect_handler {
sub data_post_handler { sub data_post_handler {
my ($self, $transaction) = @_; my ($self, $transaction) = @_;
my $match = $self->connection->notes('fcrdns_match') || 'error';
my $match = $self->connection->notes('fcrdns_match') || 0; $self->store_auth_results("iprev=$match");
$transaction->header->add('X-Fcrdns', $match ? 'Yes' : 'No', 0);
return (DECLINED); return (DECLINED);
} }
@ -182,13 +213,25 @@ sub has_reverse_dns {
my $res = $self->init_resolver(); my $res = $self->init_resolver();
my $ip = $self->qp->connection->remote_ip; my $ip = $self->qp->connection->remote_ip;
my $query = $res->query($ip) or do { my $query = $res->query($ip, 'PTR') or do {
if ($res->errorstring eq 'NXDOMAIN') { if ($res->errorstring eq 'NXDOMAIN') {
$self->adjust_karma(-1); $self->adjust_karma(-1);
$self->connection->notes('fcrdns_match', 'permerror');
$self->log(LOGINFO, "fail, no rDNS: " . $res->errorstring); $self->log(LOGINFO, "fail, no rDNS: " . $res->errorstring);
return; return;
} }
$self->log(LOGINFO, "fail, error getting rDNS: " . $res->errorstring); if ( $res->errorstring eq 'SERVFAIL' ) {
$self->log(LOGINFO, "fail, error getting rDNS: " . $res->errorstring);
$self->connection->notes('fcrdns_match', 'temperror');
}
elsif ( $res->errorstring eq 'NOERROR' ) {
$self->log(LOGINFO, "fail, no PTR (NOERROR)" );
$self->connection->notes('fcrdns_match', 'permerror');
}
else {
$self->connection->notes('fcrdns_match', 'fail');
$self->log(LOGINFO, "fail, error getting rDNS: " . $res->errorstring);
};
return; return;
}; };
@ -203,6 +246,7 @@ sub has_reverse_dns {
if (!$hits) { if (!$hits) {
$self->adjust_karma(-1); $self->adjust_karma(-1);
$self->log(LOGINFO, "fail, no PTR records"); $self->log(LOGINFO, "fail, no PTR records");
$self->connection->notes('fcrdns_match', 'permerror');
return; return;
} }
@ -218,11 +262,13 @@ sub has_forward_dns {
foreach my $host (keys %{$self->{_args}{ptr_hosts}}) { foreach my $host (keys %{$self->{_args}{ptr_hosts}}) {
$host .= '.' if '.' ne substr($host, -1, 1); # fully qualify name $host .= '.' if '.' ne substr($host, -1, 1); # fully qualify name
my $query = $res->search($host) or do { my $query = $res->query($host) or do {
if ($res->errorstring eq 'NXDOMAIN') { if ($res->errorstring eq 'NXDOMAIN') {
$self->connection->notes('fcrdns_match', 'permerror');
$self->log(LOGDEBUG, "host $host does not exist"); $self->log(LOGDEBUG, "host $host does not exist");
next; next;
} }
$self->connection->notes('fcrdns_match', 'fail');
$self->log(LOGDEBUG, "query for $host failed (", $self->log(LOGDEBUG, "query for $host failed (",
$res->errorstring, ")"); $res->errorstring, ")");
next; next;
@ -235,11 +281,13 @@ sub has_forward_dns {
$self->check_ip_match($rr->address) and return 1; $self->check_ip_match($rr->address) and return 1;
} }
if ($hits) { if ($hits) {
$self->connection->notes('fcrdns_match', 'fail');
$self->log(LOGDEBUG, "PTR host has forward DNS") if $hits; $self->log(LOGDEBUG, "PTR host has forward DNS") if $hits;
return 1; return 1;
} }
} }
$self->adjust_karma(-1); $self->adjust_karma(-1);
$self->connection->notes('fcrdns_match', 'fail');
$self->log(LOGINFO, "fail, no PTR hosts have forward DNS"); $self->log(LOGINFO, "fail, no PTR hosts have forward DNS");
return; return;
} }
@ -250,7 +298,7 @@ sub check_ip_match {
if ($ip eq $self->qp->connection->remote_ip) { if ($ip eq $self->qp->connection->remote_ip) {
$self->log(LOGDEBUG, "forward ip match"); $self->log(LOGDEBUG, "forward ip match");
$self->connection->notes('fcrdns_match', 1); $self->connection->notes('fcrdns_match', 'pass');
$self->adjust_karma(1); $self->adjust_karma(1);
return 1; return 1;
} }
@ -262,7 +310,7 @@ sub check_ip_match {
if ($dns_net eq $rem_net) { if ($dns_net eq $rem_net) {
$self->log(LOGNOTICE, "forward network match"); $self->log(LOGNOTICE, "forward network match");
$self->connection->notes('fcrdns_match', 1); $self->connection->notes('fcrdns_match', 'pass');
return 1; return 1;
} }
return; return;

View File

@ -133,10 +133,12 @@ sub mail_handler {
return (DECLINED, "SPF - no response"); return (DECLINED, "SPF - no response");
} }
$self->store_auth_results("spf=$code smtp.mailfrom=".$sender->host);
if ($code eq 'pass') { if ($code eq 'pass') {
$self->adjust_karma(1); $self->adjust_karma(1);
$transaction->notes('spf_pass_host', lc $sender->host); $transaction->notes('spf_pass_host', lc $sender->host);
$self->log(LOGINFO, "pass, $code: $why"); $self->log(LOGINFO, "pass, $why");
return (DECLINED); return (DECLINED);
} }
@ -235,8 +237,6 @@ sub data_post_handler {
$transaction->header->add('Received-SPF', $result->received_spf_header, 0); $transaction->header->add('Received-SPF', $result->received_spf_header, 0);
# consider also adding SPF status to Authentication-Results header
return DECLINED; return DECLINED;
} }