our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
sub
new {
my
$class
=
shift
;
my
$mailsaobject
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
$self
=
$class
->SUPER::new(
$mailsaobject
);
bless
(
$self
,
$class
);
$self
->register_eval_rule(
"check_dkim_signed"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_arc_signed"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_dkim_valid"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_arc_valid"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_dkim_valid_author_sig"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_dkim_testing"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dkim_valid_envelopefrom"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_dkim_adsp"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dkim_dependable"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_for_dkim_welcomelist_from"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_for_dkim_whitelist_from"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_for_def_dkim_welcomelist_from"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_for_def_dkim_whitelist_from"
,
$Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS
);
$self
->register_eval_rule(
"check_dkim_verified"
);
$self
->register_eval_rule(
"check_dkim_signall"
);
$self
->register_eval_rule(
"check_dkim_signsome"
);
$self
->set_config(
$mailsaobject
->{conf});
return
$self
;
}
sub
set_config {
my
(
$self
,
$conf
) =
@_
;
my
@cmds
;
push
(
@cmds
, {
setting
=>
'welcomelist_from_dkim'
,
aliases
=> [
'whitelist_from_dkim'
],
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
unless
(
$value
=~ /^(\S+)(?:\s+(\S+))?$/) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
my
$address
= $1;
my
$sdid
=
defined
$2 ? $2 :
''
;
$address
=~ s/(\@[^@]*)\z/
lc
($1)/e;
$self
->{parser}->add_to_addrlist_dkim(
'welcomelist_from_dkim'
,
$address
,
lc
$sdid
);
}
});
push
(
@cmds
, {
setting
=>
'def_welcomelist_from_dkim'
,
aliases
=> [
'def_whitelist_from_dkim'
],
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
unless
(
$value
=~ /^(\S+)(?:\s+(\S+))?$/) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
my
$address
= $1;
my
$sdid
=
defined
$2 ? $2 :
''
;
$address
=~ s/(\@[^@]*)\z/
lc
($1)/e;
$self
->{parser}->add_to_addrlist_dkim(
'def_welcomelist_from_dkim'
,
$address
,
lc
$sdid
);
}
});
push
(
@cmds
, {
setting
=>
'unwelcomelist_from_dkim'
,
aliases
=> [
'unwhitelist_from_dkim'
],
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
unless
(
$value
=~ /^(\S+)(?:\s+(\S+))?$/) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
my
$address
= $1;
my
$sdid
=
defined
$2 ? $2 :
''
;
$address
=~ s/(\@[^@]*)\z/
lc
($1)/e;
$self
->{parser}->remove_from_addrlist_dkim(
'welcomelist_from_dkim'
,
$address
,
lc
$sdid
);
$self
->{parser}->remove_from_addrlist_dkim(
'def_welcomelist_from_dkim'
,
$address
,
lc
$sdid
);
}
});
push
(
@cmds
, {
setting
=>
'adsp_override'
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
local
($1,$2);
unless
(
defined
$value
&&
$value
!~ /^$/) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
unless
(
$value
=~ /^ \@? ( [
*a
-z0-9._-]+ )
(?: \s+ (nxdomain|unknown|all|discardable|
custom_low|custom_med|custom_high) )?$/ix) {
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
my
$domain
=
lc
$1;
my
$adsp
= $2;
$adsp
=
'discardable'
if
!
defined
$adsp
;
$adsp
=
lc
$adsp
;
if
(
$adsp
eq
'custom_low'
) {
$adsp
=
'1'
}
elsif
(
$adsp
eq
'custom_med'
) {
$adsp
=
'2'
}
elsif
(
$adsp
eq
'custom_high'
) {
$adsp
=
'3'
}
else
{
$adsp
=
uc
substr
(
$adsp
,0,1) }
$self
->{parser}->{conf}->{adsp_override}->{
$domain
} =
$adsp
;
}
});
push
(
@cmds
, {
setting
=>
'dkim_minimum_key_bits'
,
default
=> 1024,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
,
});
push
(
@cmds
, {
setting
=>
'dkim_timeout'
,
is_admin
=> 1,
default
=> 5,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
});
$conf
->{parser}->register_commands(\
@cmds
);
}
sub
check_dkim_signed {
my
(
$self
,
$pms
,
$full_ref
,
@acceptable_domains
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
my
$result
= 0;
if
(!
$pms
->{dkim_signed}) {
}
elsif
(!
@acceptable_domains
) {
$result
= 1;
}
else
{
$result
=
$self
->_check_dkim_signed_by(
$pms
,0,0,\
@acceptable_domains
);
}
return
$result
;
}
sub
check_arc_signed {
my
(
$self
,
$pms
,
$full_ref
,
@acceptable_domains
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{arc_checked_signature};
my
$result
= 0;
if
(!
$pms
->{arc_signed}) {
}
elsif
(!
@acceptable_domains
) {
$result
= 1;
}
return
$result
;
}
sub
check_dkim_valid {
my
(
$self
,
$pms
,
$full_ref
,
@acceptable_domains
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
my
$result
= 0;
if
(!
$pms
->{dkim_valid}) {
}
elsif
(!
@acceptable_domains
) {
$result
= 1;
}
else
{
$result
=
$self
->_check_dkim_signed_by(
$pms
,1,0,\
@acceptable_domains
);
}
return
$result
;
}
sub
check_arc_valid {
my
(
$self
,
$pms
,
$full_ref
,
@acceptable_domains
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{arc_checked_signature};
my
$result
= 0;
if
(!
$pms
->{arc_valid}) {
}
elsif
(!
@acceptable_domains
) {
$result
= 1;
}
return
$result
;
}
sub
check_dkim_valid_author_sig {
my
(
$self
,
$pms
,
$full_ref
,
@acceptable_domains
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
my
$result
= 0;
if
(!%{
$pms
->{dkim_has_valid_author_sig}}) {
}
else
{
$result
=
$self
->_check_dkim_signed_by(
$pms
,1,1,\
@acceptable_domains
);
}
return
$result
;
}
sub
check_dkim_valid_envelopefrom {
my
(
$self
,
$pms
,
$full_ref
) =
@_
;
my
$result
= 0;
my
(
$envfrom
) = (
$pms
->get(
'EnvelopeFrom:addr'
)||
''
) =~ /\@(\S+)/;
return
$result
if
!
defined
$envfrom
;
$envfrom
=
lc
$envfrom
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
if
(!
$pms
->{dkim_valid}) {
}
else
{
$result
=
$self
->_check_dkim_signed_by(
$pms
,1,0,[
$envfrom
]);
}
return
$result
;
}
sub
check_dkim_dependable {
my
(
$self
,
$pms
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
return
$pms
->{dkim_signatures_dependable};
}
sub
check_dkim_verified {
return
check_dkim_valid(
@_
);
}
sub
check_dkim_adsp {
my
(
$self
,
$pms
,
$adsp_char
,
@domains_list
) =
@_
;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
my
$result
= 0;
if
(!
$pms
->{dkim_signatures_ready}) {
}
else
{
$self
->_check_dkim_adsp(
$pms
)
if
!
$pms
->{dkim_checked_adsp};
$adsp_char
=
'NAD123'
if
$adsp_char
eq
'*'
;
if
( !(
grep
{
index
(
$adsp_char
,
$_
) >= 0 }
values
%{
$pms
->{dkim_adsp}}) ) {
}
elsif
(!
@domains_list
) {
$result
= 1;
}
else
{
local
$1;
my
%author_domains
= %{
$pms
->{dkim_author_domains}};
foreach
my
$dom
(
@domains_list
) {
if
(
$dom
=~ /^\*?\.(.*)\z/s) {
my
$doms
=
lc
$1;
if
(
$author_domains
{
$doms
} ||
(
grep
{ /\.\Q
$doms
\E\z/s }
keys
%author_domains
) ) {
$result
= 1;
last
;
}
}
else
{
if
(
$author_domains
{
lc
$dom
}) {
$result
= 1;
last
;
}
}
}
}
}
return
$result
;
}
sub
check_dkim_signsome {
my
(
$self
,
$pms
) =
@_
;
return
0;
}
sub
check_dkim_signall {
my
(
$self
,
$pms
) =
@_
;
check_dkim_adsp(
$self
,
$pms
,
'A'
);
}
sub
check_dkim_testing {
my
(
$self
,
$pms
) =
@_
;
my
$result
= 0;
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
$result
= 1
if
$pms
->{dkim_key_testing};
return
$result
;
}
sub
check_for_dkim_welcomelist_from {
my
(
$self
,
$pms
) =
@_
;
$self
->_check_dkim_welcomelist(
$pms
)
if
!
$pms
->{welcomelist_checked};
return
(
$pms
->{dkim_match_in_welcomelist_from_dkim} ||
$pms
->{dkim_match_in_welcomelist_auth}) ? 1 : 0;
}
*check_for_dkim_whitelist_from
= \
&check_for_dkim_welcomelist_from
;
sub
check_for_def_dkim_welcomelist_from {
my
(
$self
,
$pms
) =
@_
;
$self
->_check_dkim_welcomelist(
$pms
)
if
!
$pms
->{welcomelist_checked};
return
(
$pms
->{dkim_match_in_def_welcomelist_from_dkim} ||
$pms
->{dkim_match_in_def_welcomelist_auth}) ? 1 : 0;
}
*check_for_def_dkim_whitelist_from
= \
&check_for_def_dkim_welcomelist_from
;
sub
_dkim_load_modules {
my
(
$self
) =
@_
;
if
(!
$self
->{tried_loading}) {
$self
->{service_available} = 0;
my
$timemethod
=
$self
->{main}->time_method(
"dkim_load_modules"
);
my
$eval_stat
;
eval
{
} or
do
{
$eval_stat
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$eval_stat
;
};
$self
->{tried_loading} = 1;
if
(
defined
$eval_stat
) {
dbg(
"dkim: cannot load Mail::DKIM module, DKIM checks disabled: %s"
,
$eval_stat
);
}
else
{
my
$version
= Mail::DKIM::Verifier->VERSION;
if
(version->parse(
$version
) >= version->parse(0.31)) {
dbg(
"dkim: using Mail::DKIM version $version"
);
}
elsif
(version->parse(
$version
) < version->parse(0.50)) {
dbg(
"dkim: Mail::DKIM $version is older than 0.50 "
.
"ARC support will not be available, suggested upgrade to 0.50 or later!"
);
}
else
{
info(
"dkim: Mail::DKIM $version is older than the required "
.
"minimal version 0.31, suggested upgrade to 0.37 or later!"
);
}
$self
->{service_available} = 1;
my
$adsp_avail
=
if
(!
$adsp_avail
) {
}
}
eval
{
$self
->{arc_available} = 1;
} or
do
{
$eval_stat
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$eval_stat
;
if
(
defined
$eval_stat
) {
dbg(
"dkim: cannot load Mail::DKIM::ARC module, DKIM::ARC checks disabled: %s"
,
$eval_stat
);
}
$self
->{arc_available} = 0;
};
}
return
$self
->{service_available};
}
sub
_check_dkim_signed_by {
my
(
$self
,
$pms
,
$must_be_valid
,
$must_be_author_domain_signature
,
$acceptable_domains_ref
) =
@_
;
my
$result
= 0;
my
$verifier
=
$pms
->{dkim_verifier};
my
$minimum_key_bits
=
$pms
->{conf}->{dkim_minimum_key_bits};
foreach
my
$sig
(@{
$pms
->{dkim_signatures}}) {
next
if
!
defined
$sig
;
if
(
$must_be_valid
) {
next
if
(
$sig
->UNIVERSAL::can(
"result"
) ?
$sig
:
$verifier
)
->result ne
'pass'
;
next
if
$sig
->UNIVERSAL::can(
"check_expiration"
) &&
!
$sig
->check_expiration;
next
if
$minimum_key_bits
&&
$sig
->{_spamassassin_key_size} &&
$sig
->{_spamassassin_key_size} <
$minimum_key_bits
;
}
my
(
$sdid
) = (
defined
$sig
->identity)?
$sig
->identity =~ /\@(\S+)/ : (
$sig
->domain);
next
if
!
defined
$sdid
;
$sdid
=
lc
$sdid
;
if
(
$must_be_author_domain_signature
) {
next
if
!
$pms
->{dkim_author_domains}->{
$sdid
};
}
if
(!
@$acceptable_domains_ref
) {
$result
= 1;
}
else
{
foreach
my
$ad
(
@$acceptable_domains_ref
) {
if
(
$ad
=~ /^\*?\.(.*)\z/s) {
my
$d
=
lc
$1;
if
(
$sdid
eq
$d
||
$sdid
=~ /\.\Q
$d
\E\z/s) {
$result
= 1;
last
}
}
else
{
if
(
$sdid
eq
lc
$ad
) {
$result
= 1;
last
}
}
}
}
last
if
$result
;
}
return
$result
;
}
sub
_get_authors {
my
(
$self
,
$pms
) =
@_
;
my
%author_domains
;
local
$1;
my
@authors
=
grep
{
defined
$_
} (
$pms
->get(
'from:addr'
,
undef
) );
for
(
@authors
) {
$author_domains
{
lc
$1} = 1
if
/\@([^\@]+?)[ \t]*\z/s;
}
$pms
->{dkim_author_addresses} = \
@authors
;
$pms
->{dkim_author_domains} = \
%author_domains
;
}
sub
_check_dkim_signature {
my
(
$self
,
$pms
) =
@_
;
my
$conf
=
$pms
->{conf};
my
(
$verifier
,
$arc_verifier
,
@signatures
,
@arc_signatures
,
@valid_signatures
,
@arc_valid_signatures
);
$pms
->{dkim_checked_signature} = 1;
$pms
->{arc_checked_signature} = 1;
$pms
->{dkim_signatures_ready} = 0;
$pms
->{dkim_signatures_dependable} = 0;
$pms
->{dkim_signatures} = \
@signatures
;
$pms
->{dkim_valid_signatures} = \
@valid_signatures
;
$pms
->{arc_signatures} = \
@arc_signatures
;
$pms
->{arc_valid_signatures} = \
@arc_valid_signatures
;
$pms
->{dkim_signed} = 0;
$pms
->{arc_signed} = 0;
$pms
->{dkim_valid} = 0;
$pms
->{arc_valid} = 0;
$pms
->{dkim_key_testing} = 0;
$pms
->{dkim_author_sig_tempfailed} = {};
$pms
->{dkim_has_valid_author_sig} = {};
$pms
->{dkim_has_any_author_sig} = {};
my
$suppl_attrib
=
$pms
->{msg}->{suppl_attrib};
if
(
defined
$suppl_attrib
&&
exists
$suppl_attrib
->{dkim_signatures}) {
my
$provided_signatures
=
$suppl_attrib
->{dkim_signatures};
@signatures
=
@$provided_signatures
if
ref
$provided_signatures
;
$pms
->{dkim_signatures_ready} = 1;
$pms
->{dkim_signatures_dependable} = 1;
dbg(
"dkim: DKIM signatures provided by the caller, %d signatures"
,
scalar
(
@signatures
));
}
if
(
defined
$suppl_attrib
&&
exists
$suppl_attrib
->{arc_signatures}) {
my
$provided_arc_signatures
=
$suppl_attrib
->{arc_signatures};
@arc_signatures
=
@$provided_arc_signatures
if
ref
$provided_arc_signatures
;
$pms
->{arc_signatures_ready} = 1;
$pms
->{arc_signatures_dependable} = 1;
dbg(
"dkim: ARC signatures provided by the caller, %d signatures"
,
scalar
(
@arc_signatures
));
}
if
(
$pms
->{dkim_signatures_ready} or
$pms
->{arc_signatures_ready}) {
_check_valid_signature(
$self
,
$pms
,
$verifier
,
'DKIM'
, \
@signatures
)
if
$self
->{service_available};
_check_valid_signature(
$self
,
$pms
,
$arc_verifier
,
'ARC'
, \
@arc_signatures
)
if
$self
->{arc_available};
}
elsif
(!
$pms
->is_dns_available()) {
dbg(
"dkim: signature verification disabled, DNS resolving not available"
);
}
elsif
(!
$self
->_dkim_load_modules()) {
}
else
{
my
$timemethod
=
$self
->{main}->time_method(
"check_dkim_signature"
);
if
(Mail::DKIM::Verifier->VERSION >= 0.40) {
my
$edns
=
$conf
->{dns_options}->{edns};
if
(
$edns
&&
$edns
>= 1024) {
my
$res
=
$self
->{main}->{resolver};
dbg(
"dkim: providing our own resolver: %s"
,
ref
$res
);
Mail::DKIM::DNS::resolver(
$res
);
}
}
$verifier
= Mail::DKIM::Verifier->new
if
$self
->{service_available};
_check_signature(
$self
,
$pms
,
$verifier
,
'DKIM'
, \
@signatures
)
if
$self
->{service_available};
$arc_verifier
= Mail::DKIM::ARC::Verifier->new
if
$self
->{arc_available};
_check_signature(
$self
,
$pms
,
$arc_verifier
,
'ARC'
, \
@arc_signatures
)
if
$self
->{arc_available};
}
}
sub
_check_signature {
my
(
$self
,
$pms
,
$verifier
,
$type
,
$signatures
) =
@_
;
my
$sig_type
=
lc
$type
;
$self
->_get_authors(
$pms
)
if
!
$pms
->{
"${sig_type}_author_addresses"
};
my
(
@valid_signatures
);
my
$conf
=
$pms
->{conf};
if
(!
$verifier
) {
if
(
$type
eq
'DKIM'
) {
dbg(
"dkim: cannot create Mail::DKIM::Verifier object"
);
}
elsif
(
$type
eq
'ARC'
) {
dbg(
"dkim: cannot create Mail::DKIM::ARC::Verifier object"
);
}
return
;
}
else
{
if
(
$type
eq
'DKIM'
) {
$pms
->{dkim_verifier} =
$verifier
;
}
elsif
(
$type
eq
'ARC'
) {
$pms
->{arc_verifier} =
$verifier
;
}
}
eval
{
my
$str
=
$pms
->{msg}->get_pristine();
if
(
$pms
->{msg}->{line_ending} eq
"\015\012"
) {
$verifier
->PRINT(
$str
);
}
else
{
$str
=~ s/\012/\015\012/gs;
$verifier
->PRINT(
$str
);
undef
$str
;
}
1;
} or
do
{
my
$eval_stat
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$eval_stat
;
dbg(
"dkim: verification failed, intercepted error: $eval_stat"
);
return
0;
};
my
$timeout
=
$conf
->{dkim_timeout};
my
$timer
= Mail::SpamAssassin::Timeout->new(
{
secs
=>
$timeout
,
deadline
=>
$pms
->{master_deadline} });
my
$err
=
$timer
->run_and_catch(
sub
{
dbg(
"dkim: performing public $type key lookup and signature verification"
);
$verifier
->CLOSE();
@$signatures
=
$verifier
->UNIVERSAL::can(
"signatures"
) ?
$verifier
->signatures :
$verifier
->signature;
if
(would_log(
"dbg"
,
"dkim"
)) {
foreach
my
$signature
(
@$signatures
) {
dbg(
"dkim: $type signature i=%s d=%s"
,
map
(!
defined
$_
?
'(undef)'
:
$_
,
$signature
->identity,
$signature
->domain
)
);
}
}
});
if
(
$timer
->timed_out()) {
dbg(
"dkim: public key lookup or verification timed out after %s s"
,
$timeout
);
}
elsif
(
$err
) {
chomp
$err
;
dbg(
"dkim: $type public key lookup or verification failed: $err"
);
}
if
(
$type
eq
'DKIM'
) {
$pms
->{dkim_signatures_ready} = 1;
if
(!
@$signatures
|| !
$pms
->{tests_already_hit}->{
'__TRUNCATED'
}) {
$pms
->{dkim_signatures_dependable} = 1;
}
_check_valid_signature(
$self
,
$pms
,
$verifier
,
'DKIM'
, \
@$signatures
)
if
$self
->{service_available};
}
elsif
(
$type
eq
'ARC'
) {
$pms
->{arc_signatures_ready} = 1;
if
(!
@$signatures
|| !
$pms
->{tests_already_hit}->{
'__TRUNCATED'
}) {
$pms
->{arc_signatures_dependable} = 1;
}
_check_valid_signature(
$self
,
$pms
,
$verifier
,
'ARC'
, \
@$signatures
)
if
$self
->{arc_available};
}
}
sub
_check_valid_signature {
my
(
$self
,
$pms
,
$verifier
,
$type
,
$signatures
) =
@_
;
my
$sig_type
=
lc
$type
;
$self
->_get_authors(
$pms
)
if
!
$pms
->{
"${sig_type}_author_addresses"
};
my
(
@valid_signatures
);
my
$conf
=
$pms
->{conf};
if
(
$pms
->{
"${sig_type}_signatures_ready"
}) {
my
$sig_result_supported
;
my
$minimum_key_bits
=
$conf
->{dkim_minimum_key_bits};
foreach
my
$signature
(
@$signatures
) {
next
if
!
defined
$signature
;
$sig_result_supported
=
$signature
->UNIVERSAL::can(
"result_detail"
);
next
if
!
defined
$signature
->selector ||
$signature
->selector eq
""
;
my
(
$info
,
$valid
,
$expired
);
$valid
=
(
$sig_result_supported
?
$signature
:
$verifier
)->result eq
'pass'
;
$info
=
$valid
?
'VALID'
:
'FAILED'
;
if
(
$valid
&&
$signature
->UNIVERSAL::can(
"check_expiration"
)) {
$expired
= !
$signature
->check_expiration;
$info
.=
' EXPIRED'
if
$expired
;
}
my
$key_size
;
if
(
$valid
&& !
$expired
&&
$minimum_key_bits
) {
$key_size
=
eval
{
my
$pk
=
$signature
->get_public_key;
$pk
&&
$pk
->cork &&
$pk
->cork->size * 8 };
if
(
$key_size
) {
$signature
->{_spamassassin_key_size} =
$key_size
;
$info
.=
" WEAK($key_size)"
if
$key_size
<
$minimum_key_bits
;
}
}
push
(
@valid_signatures
,
$signature
)
if
$valid
&& !
$expired
;
my
(
$d
) = (
defined
$signature
->identity)?
$signature
->identity =~ /\@(\S+)/ : (
$signature
->domain);
if
(!
defined
$d
) {
}
else
{
$d
=
lc
$d
;
if
(
$pms
->{
"${sig_type}_author_domains"
}->{
$d
}) {
$pms
->{
"${sig_type}_has_any_author_sig"
}->{
$d
} = 1;
if
(
$valid
&& !
$expired
&&
$key_size
&&
$key_size
>=
$minimum_key_bits
) {
$pms
->{
"${sig_type}_has_valid_author_sig"
}->{
$d
} = 1;
}
elsif
( (
$sig_result_supported
?
$signature
:
$verifier
)->result_detail
=~ /\b(?:timed out|SERVFAIL)\b/i) {
$pms
->{
"${sig_type}_author_sig_tempfailed"
}->{
$d
} = 1;
}
}
}
if
(
$type
eq
'DKIM'
) {
if
(would_log(
"dbg"
,
"dkim"
)) {
dbg(
"dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s"
,
$info
,
$signature
->isa(
'Mail::DKIM::DkSignature'
) ?
'DK'
:
'DKIM'
,
map
(!
defined
$_
?
'(undef)'
:
$_
,
$signature
->identity,
$d
,
$signature
->selector,
$signature
->algorithm,
scalar
(
$signature
->canonicalization),
$key_size
?
"key_bits=$key_size"
:
"unknown key size"
,
(
$sig_result_supported
?
$signature
:
$verifier
)->result ),
defined
$d
&&
$pms
->{dkim_author_domains}->{
$d
}
?
'matches author domain'
:
'does not match author domain'
,
);
}
}
elsif
(
$type
eq
'ARC'
) {
if
(would_log(
"dbg"
,
"dkim"
)) {
dbg(
"dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s"
,
$info
,
$type
,
map
(!
defined
$_
?
'(undef)'
:
$_
,
$signature
->identity,
$d
,
$signature
->selector,
$signature
->algorithm,
scalar
(
$signature
->canonicalization),
$key_size
?
"key_bits=$key_size"
:
"unknown key size"
,
(
$sig_result_supported
?
$signature
:
$verifier
)->result ),
defined
$d
&&
$pms
->{arc_author_domains}->{
$d
}
?
'matches author domain'
:
'does not match author domain'
,
);
}
}
}
if
(
@valid_signatures
) {
if
(
$type
eq
'DKIM'
) {
$pms
->{dkim_signed} = 1;
$pms
->{dkim_valid} = 1;
my
(
%seen1
,
%seen2
,
%seen3
,
@identity_list
,
@domain_list
,
@selector_list
);
@identity_list
=
grep
(
defined
$_
&&
$_
ne
''
&& !
$seen1
{
$_
}++,
map
(
$_
->identity,
@valid_signatures
));
@domain_list
=
grep
(
defined
$_
&&
$_
ne
''
&& !
$seen2
{
$_
}++,
map
(
$_
->domain,
@valid_signatures
));
@selector_list
=
grep
(
defined
$_
&&
$_
ne
''
&& !
$seen3
{
$_
}++,
map
(
$_
->selector,
@valid_signatures
));
$pms
->set_tag(
'DKIMIDENTITY'
,
@identity_list
== 1 ?
$identity_list
[0] : \
@identity_list
);
$pms
->set_tag(
'DKIMDOMAIN'
,
@domain_list
== 1 ?
$domain_list
[0] : \
@domain_list
);
$pms
->set_tag(
'DKIMSELECTOR'
,
@selector_list
== 1 ?
$selector_list
[0] : \
@selector_list
);
}
elsif
(
$type
eq
'ARC'
) {
$pms
->{arc_signed} = 1;
$pms
->{arc_valid} = 1;
}
my
$sig
=
$valid_signatures
[0];
my
$sig_res
= (
$sig_result_supported
?
$sig
:
$verifier
)->result_detail;
dbg(
"dkim: $type signature verification result: %s"
,
uc
(
$sig_res
));
}
elsif
(
@$signatures
) {
if
(
$type
eq
'DKIM'
) {
$pms
->{dkim_signed} = 1;
}
elsif
(
$type
eq
'ARC'
) {
$pms
->{arc_signed} = 1;
}
my
$sig
=
@$signatures
[0];
my
$sig_res
= (
$sig_result_supported
?
$sig
:
$verifier
)->result_detail;
dbg(
"dkim: $type signature verification result: %s"
,
uc
(
$sig_res
));
}
else
{
dbg(
"dkim: $type signature verification result: none"
);
}
}
}
sub
_check_dkim_adsp {
my
(
$self
,
$pms
) =
@_
;
$pms
->{dkim_checked_adsp} = 1;
$pms
->{dkim_adsp} = {};
my
$practices_as_string
=
''
;
$self
->_get_authors(
$pms
)
if
!
$pms
->{dkim_author_addresses};
my
@author_domains
=
grep
{ /.\.[a-z-]{2,}\z/si }
keys
%{
$pms
->{dkim_author_domains}};
my
%label
=
(
'D'
=>
'discardable'
,
'A'
=>
'all'
,
'U'
=>
'unknown'
,
'N'
=>
'nxdomain'
,
'1'
=>
'custom_low'
,
'2'
=>
'custom_med'
,
'3'
=>
'custom_high'
);
$self
->_check_dkim_signature(
$pms
)
if
!
$pms
->{dkim_checked_signature};
if
(!
$pms
->{dkim_signatures_ready}) {
dbg(
"dkim: adsp not retrieved, signatures not obtained"
);
}
elsif
(!
@author_domains
) {
dbg(
"dkim: adsp not retrieved, no author f.q. domain name"
);
$practices_as_string
=
'no author domains, ignored'
;
}
else
{
foreach
my
$author_domain
(
@author_domains
) {
my
$adsp
;
if
(
$pms
->{dkim_has_valid_author_sig}->{
$author_domain
}) {
dbg(
"dkim: adsp not retrieved, author domain signature is valid"
);
$practices_as_string
=
'valid a. d. signature'
;
}
elsif
(
$pms
->{dkim_author_sig_tempfailed}->{
$author_domain
}) {
dbg(
"dkim: adsp ignored, tempfail varifying author domain signature"
);
$practices_as_string
=
'pub key tempfailed, ignored'
;
}
elsif
(
$pms
->{dkim_has_any_author_sig}->{
$author_domain
} &&
!
$pms
->{dkim_signatures_dependable}) {
dbg(
"dkim: adsp ignored, message was truncated, "
.
"invalid author domain signature"
);
$practices_as_string
=
'truncated, ignored'
;
}
else
{
my
$matched_key
;
my
$p
=
$pms
->{conf}->{adsp_override};
if
(
$p
) {
my
@d
=
split
(/\./,
$author_domain
);
@d
=
map
{
shift
@d
;
join
(
'.'
,
'*'
,
@d
) } (0..
$#d
);
for
my
$key
(
$author_domain
,
@d
) {
$adsp
=
$p
->{
$key
};
if
(
defined
$adsp
) {
$matched_key
=
$key
;
last
}
}
}
if
(
defined
$adsp
) {
dbg(
"dkim: adsp override for domain %s"
,
$author_domain
);
$practices_as_string
=
'override'
;
$practices_as_string
.=
" by $matched_key"
if
$matched_key
ne
$author_domain
;
}
elsif
(!
$pms
->is_dns_available()) {
dbg(
"dkim: adsp not retrieved, DNS resolving not available"
);
}
elsif
(!
$self
->_dkim_load_modules()) {
dbg(
"dkim: adsp not retrieved, module Mail::DKIM not available"
);
}
else
{
my
$timemethod
=
$self
->{main}->time_method(
"check_dkim_adsp"
);
my
$practices
;
my
$timeout
=
$pms
->{conf}->{dkim_timeout};
my
$timer
= Mail::SpamAssassin::Timeout->new(
{
secs
=>
$timeout
,
deadline
=>
$pms
->{master_deadline} });
my
$err
=
$timer
->run_and_catch(
sub
{
eval
{
if
(Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can(
"fetch"
)) {
my
$author_domain_ace
= idn_to_ascii(
$author_domain
);
dbg(
"dkim: adsp: performing lookup on _adsp._domainkey.%s"
,
$author_domain_ace
);
my
$res
=
$self
->{main}->{resolver}->get_resolver;
$practices
= Mail::DKIM::AuthorDomainPolicy->fetch(
Protocol
=>
"dns"
,
Domain
=>
$author_domain_ace
,
DnsResolver
=>
$res
);
}
1;
} or
do
{
my
$eval_stat
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$eval_stat
;
dbg(
"dkim: adsp: fetch or parse on domain %s failed: %s"
,
$author_domain
,
$eval_stat
);
undef
$practices
;
};
});
if
(
$timer
->timed_out()) {
dbg(
"dkim: adsp lookup on domain %s timed out after %s seconds"
,
$author_domain
,
$timeout
);
}
elsif
(
$err
) {
chomp
$err
;
dbg(
"dkim: adsp lookup on domain %s failed: %s"
,
$author_domain
,
$err
);
}
else
{
my
$sp
;
(
$sp
) =
$practices
->policy
if
$practices
;
if
(!
defined
$sp
||
$sp
eq
''
) {
dbg(
"dkim: signing practices on %s unavailable"
,
$author_domain
);
$adsp
=
'U'
;
$practices_as_string
=
'dns: no result'
;
}
else
{
$adsp
=
$sp
eq
"unknown"
?
'U'
:
$sp
eq
"all"
?
'A'
:
$sp
eq
"discardable"
?
'D'
:
$sp
eq
"strict"
?
'D'
:
uc
(
$sp
) eq
"NXDOMAIN"
?
'N'
:
'U'
;
$practices_as_string
=
'dns: '
.
$sp
;
}
}
}
}
$pms
->{dkim_adsp}->{
$author_domain
} =
$adsp
if
defined
$adsp
;
dbg(
"dkim: adsp result: %s (%s), author domain '%s'"
,
!
defined
(
$adsp
) ?
'-'
:
$adsp
.
'/'
.
$label
{
$adsp
},
$practices_as_string
,
$author_domain
);
}
}
}
sub
_check_dkim_welcomelist {
my
(
$self
,
$pms
) =
@_
;
$pms
->{welcomelist_checked} = 1;
$self
->_get_authors(
$pms
)
if
!
$pms
->{dkim_author_addresses};
my
$authors_str
=
join
(
", "
, @{
$pms
->{dkim_author_addresses}});
if
(
$authors_str
eq
''
) {
dbg(
"dkim: check_dkim_weclomelist: could not find author address"
);
return
;
}
my
@acceptable_sdid_tuples
;
$self
->_wlcheck_acceptable_signature(
$pms
, \
@acceptable_sdid_tuples
,
'def_welcomelist_from_dkim'
);
$self
->_wlcheck_author_signature(
$pms
, \
@acceptable_sdid_tuples
,
'def_welcomelist_auth'
);
$self
->_wlcheck_acceptable_signature(
$pms
, \
@acceptable_sdid_tuples
,
'welcomelist_from_dkim'
);
$self
->_wlcheck_author_signature(
$pms
, \
@acceptable_sdid_tuples
,
'welcomelist_auth'
);
if
(!
@acceptable_sdid_tuples
) {
dbg(
"dkim: no wl entries match author %s, no need to verify sigs"
,
$authors_str
);
return
;
}
return
unless
$self
->check_dkim_valid(
$pms
) || would_log(
"dbg"
,
"dkim"
);
return
unless
$pms
->{dkim_signatures_ready};
my
(
$any_match_at_all
,
$any_match_by_wl_ref
) =
_wlcheck_list(
$self
,
$pms
, \
@acceptable_sdid_tuples
);
my
(
@valid
,
@fail
);
foreach
my
$wl
(
keys
%$any_match_by_wl_ref
) {
my
$match
=
$any_match_by_wl_ref
->{
$wl
};
if
(
defined
$match
) {
$pms
->{
"dkim_match_in_$wl"
} = 1
if
$match
;
push
(@{
$match
? \
@valid
: \
@fail
},
"$wl/$match"
);
}
}
if
(
@valid
) {
dbg(
"dkim: author %s, WELCOMELISTED by %s"
,
$authors_str
,
join
(
", "
,
@valid
));
}
elsif
(
@fail
) {
dbg(
"dkim: author %s, found in %s BUT IGNORED"
,
$authors_str
,
join
(
", "
,
@fail
));
}
else
{
dbg(
"dkim: author %s, not in any dkim welcomelist"
,
$authors_str
);
}
}
sub
_wlcheck_acceptable_signature {
my
(
$self
,
$pms
,
$acceptable_sdid_tuples_ref
,
$wl
) =
@_
;
my
$wl_ref
=
$pms
->{conf}->{
$wl
};
foreach
my
$author
(@{
$pms
->{dkim_author_addresses}}) {
my
$author_lc
=
lc
(
$author
);
foreach
my
$welcome_addr
(
keys
%$wl_ref
) {
my
$wl_addr_ref
=
$wl_ref
->{
$welcome_addr
};
if
(
$author_lc
=~ /
$wl_addr_ref
->{re}/) {
foreach
my
$sdid
(@{
$wl_addr_ref
->{domain}}) {
push
(
@$acceptable_sdid_tuples_ref
, [
$author
,
$sdid
,
$wl
,
$welcome_addr
]);
}
}
}
}
}
sub
_wlcheck_author_signature {
my
(
$self
,
$pms
,
$acceptable_sdid_tuples_ref
,
$wl
) =
@_
;
my
$wl_ref
=
$pms
->{conf}->{
$wl
};
foreach
my
$author
(@{
$pms
->{dkim_author_addresses}}) {
my
$author_lc
=
lc
(
$author
);
foreach
my
$welcome_addr
(
keys
%$wl_ref
) {
if
(
$author_lc
=~ /
$wl_ref
->{
$welcome_addr
}/) {
push
(
@$acceptable_sdid_tuples_ref
, [
$author
,
undef
,
$wl
,
$welcome_addr
]);
}
}
}
}
sub
_wlcheck_list {
my
(
$self
,
$pms
,
$acceptable_sdid_tuples_ref
) =
@_
;
my
%any_match_by_wl
;
my
$any_match_at_all
= 0;
my
$verifier
=
$pms
->{dkim_verifier};
my
$minimum_key_bits
=
$pms
->{conf}->{dkim_minimum_key_bits};
foreach
my
$signature
(@{
$pms
->{dkim_signatures}}) {
next
if
!
defined
$signature
;
my
$sig_result_supported
=
$signature
->UNIVERSAL::can(
"result_detail"
);
next
if
!
defined
$signature
->selector ||
$signature
->selector eq
""
;
my
(
$info
,
$valid
,
$expired
,
$key_size_weak
);
$valid
=
(
$sig_result_supported
?
$signature
:
$verifier
)->result eq
'pass'
;
$info
=
$valid
?
'VALID'
:
'FAILED'
;
if
(
$valid
&&
$signature
->UNIVERSAL::can(
"check_expiration"
)) {
$expired
= !
$signature
->check_expiration;
$info
.=
' EXPIRED'
if
$expired
;
}
if
(
$valid
&& !
$expired
&&
$minimum_key_bits
) {
my
$key_size
=
$signature
->{_spamassassin_key_size};
if
(
$key_size
&&
$key_size
<
$minimum_key_bits
) {
$info
.=
" WEAK($key_size)"
;
$key_size_weak
= 1;
}
}
my
(
$sdid
) = (
defined
$signature
->identity)?
$signature
->identity =~ /\@(\S+)/ : (
$signature
->domain);
$sdid
=
lc
$sdid
if
defined
$sdid
;
my
%tried_authors
;
foreach
my
$entry
(
@$acceptable_sdid_tuples_ref
) {
my
(
$author
,
$acceptable_sdid
,
$wl
,
$welcome_addr
) =
@$entry
;
local
$1;
my
$author_domain
=
$author
!~ /\@([^\@]+)\z/s ?
''
:
lc
$1;
$tried_authors
{
$author
} = 1;
my
$matches
= 0;
if
(!
defined
$sdid
) {
}
elsif
(!
defined
$acceptable_sdid
||
$acceptable_sdid
eq
''
) {
$matches
= 1
if
$sdid
eq
$author_domain
;
}
else
{
$acceptable_sdid
= $1
if
$acceptable_sdid
=~ /\@([^\@]*)\z/s;
if
(
$acceptable_sdid
=~ s/^\*?\.//s) {
$matches
= 1
if
$sdid
=~ /\.\Q
$acceptable_sdid
\E\z/si;
}
else
{
$matches
= 1
if
$sdid
eq
lc
$acceptable_sdid
;
}
}
if
(
$matches
) {
if
(would_log(
"dbg"
,
"dkim"
)) {
if
(
$sdid
eq
$author_domain
) {
dbg(
"dkim: %s author domain signature by %s, MATCHES %s %s"
,
$info
,
$sdid
,
$wl
,
$welcome_addr
);
}
else
{
dbg(
"dkim: %s third-party signature by %s, author domain %s, "
.
"MATCHES %s %s"
,
$info
,
$sdid
,
$author_domain
,
$wl
,
$welcome_addr
);
}
}
$any_match_by_wl
{
$wl
} =
''
if
!
exists
$any_match_by_wl
{
$wl
};
}
$matches
= 0
if
!
$valid
||
$expired
||
$key_size_weak
;
if
(
$matches
) {
$any_match_at_all
= 1;
$any_match_by_wl
{
$wl
} =
$sdid
;
}
}
dbg(
"dkim: %s signature by %s, author %s, no valid matches"
,
$info
,
defined
$sdid
?
$sdid
:
'(undef)'
,
join
(
", "
,
keys
%tried_authors
))
if
!
$any_match_at_all
;
}
return
(
$any_match_at_all
, \
%any_match_by_wl
);
}
sub
has_arc { 1 }
1;