NAME
Mail::DKIM::Iterator - Iterative DKIM validation or signing.
SYNOPSIS
# ---- Verify all DKIM signature headers found within a mail -----------
my $mailfile = $ARGV[0];
use Mail::DKIM::Iterator;
use Net::DNS;
my %dnscache;
my $res = Net::DNS::Resolver->new;
# Create a new Mail::DKIM::Iterator object.
# Feed parts from the mail and results from DNS lookups into the object
# until we have the final result.
open( my $fh,'<',$mailfile) or die $!;
my $dkim = Mail::DKIM::Iterator->new(dns => \%dnscache);
my $rv;
my @todo = \'';
while (@todo) {
my $todo = shift(@todo);
if (ref($todo)) {
# need more data from mail
if (read($fh,$buf,8192)) {
($rv,@todo) = $dkim->next($buf);
} else {
($rv,@todo) = $dkim->next('');
}
} else {
# need a DNS lookup
if (my $q = $res->query($todo,'TXT')) {
# successful lookup
($rv,@todo) = $dkim->next({
$todo => [
map { $_->type eq 'TXT' ? ($_->txtdata) : () }
$q->answer
]
});
} else {
# failed lookup
($rv,@todo) = $dkim->next({ $todo => undef });
}
}
}
# This final result consists of a VerifyRecord for each DKIM signature
# in the header, which provides access to the status. Status is one of
# of DKIM_FAIL, DKIM_FAIL, DKIM_PERMERROR, DKIM_TEMPERROR, DKIM_NEUTRAL or
# DKIM_POLICY. In case of error $record->error contains a string
# representation of the error.
for(@$rv) {
my $status = $_->status;
my $name = $_->domain;
if (!defined $status) {
print STDERR "$mailfile: $name UNKNOWN\n";
} elsif ($status == DKIM_PASS) {
# fully validated
print STDERR "$mailfile: $name OK ".$_->warning".\n";
} elsif ($status == DKIM_FAIL) {
# hard error
print STDERR "$mailfile: $name FAIL ".$_->error."\n";
} else {
# soft-fail, temp-fail, invalid-header
print STDERR "$mailfile: $name $status ".$_->error."\n";
}
}
# ---- Create signature for a mail -------------------------------------
my $mailfile = $ARGV[0];
use Mail::DKIM::Iterator;
my $dkim = Mail::DKIM::Iterator->new(sign => {
c => 'relaxed/relaxed',
a => 'rsa-sha1',
d => 'example.com',
s => 'foobar',
':key' => PEM string for private key or Crypt::OpenSSL::RSA object
});
open(my $fh,'<',$mailfile) or die $!;
my $rv;
my @todo = \'';
while (@todo) {
my $todo = shift @todo;
die "DNS lookups should not be needed here" if !ref($todo);
# need more data from mail
if (read($fh,$buf,8192)) {
($rv,@todo) = $dkim->next($buf);
} else {
($rv,@todo) = $dkim->next('');
}
}
for(@$rv) {
my $status = $_->status;
my $name = $_->domain;
if (!defined $status) {
print STDERR "$mailfile: $name UNKNOWN\n";
} elsif (status != DKIM_PASS) {
print STDERR "$mailfile: $name $status - ".$_->error."\n";
} else {
# show signature
print $_->signature;
}
}
DESCRIPTION
With this module one can validate DKIM Signatures in mails and also create DKIM signatures for mails.
The main difference to Mail::DKIM is that the validation can be done iterative, that is the mail can be streamed into the object and if DNS lookups are necessary their results can be added to the DKIM object asynchronously. There are no blocking operation or waiting for input, everything is directly driven by the user/application feeding the DKIM object with data.
This module implements only DKIM according to RFC 6376. It does not support the historic DomainKeys standard (RFC 4870).
The following methods are relevant. For details of their use see the examples in the SYNOPSIS.
- new(%args) -> $dkim
-
This will create a new object. The following arguments are supported
- dns => \%hash
-
A hash with the DNS name as key and the DKIM record for this name as value. This can be used as a common DNS cache shared over multiple instances of the class. If none is given only a local hash will be created inside the object.
- sign => \@dkim_sig
-
List of DKIM signatures which should be used for signing the mail (usually only a single one). These can be given as string or hash (see
parse_signature
below). These DKIM signatures are only used to collect the relevant information from the header and body of the mail, the actual signing is done in the SignRecord object (see below). - sign_and_verify => 0|1
-
Usually it either signs the mail (if
sign
is given) or validates signatures inside the mail. When this option is true it will validate existing signatures additionally to creating new signatures ifsign
is used. - filter => $sub
-
A filter function which gets applied to all signatures. Signatures not matching the filter will be removed. The function is called as
$sub->(\%sig,$header)
where%sig
is the signature hash and$header
the header of the mail (which can be considered the same over all calls of$sub
). Typically this is used to exclude any signatures which don't match the domain of the From header, i.e. check against$sig{d}
.
- $dkim->next([ $mailchunk | \%dns ]*) -> ($rv,@todo)
-
This is used to add new information to the DKIM object. These information can be a new chunk from the mail (string), the signal for end of mail input (empty string
''
) or a mapping between the name and the record for a DKIM key.If there are still things todo to get the final result
@todo
will get the necessary instructions, either as a string containing a DNS name which should be used to lookup a DKIM key record, or a reference to a scalar\''
to signal that more data from the mail are needed.$rv
might already contain preliminary results.Once the final result could be computed
@todo
will be empty and$rv
will contain the results as a list. Each of the objects in the list is either a VerifyRecord (in case of DKIM verification) or a SignRecord (in case of DKIM signing).Both VerifyRecord and SignRecord have the following methods:
- status - undef if no DKIM result is yet known for the record (preliminary result). Otherwise any of DKIM_PASS, DKIM_FAIL, DKIM_NEUTRAL, DKIM_TEMPERROR, DKIM_POLICY, DKIM_PERMERROR.
- error - an error description in case the status shows an error, i.e. with all status values except undef and DKIM_PASS.
- sig - the DKIM signature as hash
- domain - the domain value from the DKIM signature
- dnsname - the dnsname value, i.e. based on domain and selector
A SignRecord has additionally the following methods:
A VerifyRecord has additionally the following methods:
- warning - possible warnings if DKIM_PASS
-
Currently this is used to provide information if critical header fields in the mail are not convered by the signature and thus might have been changed or added. It will also warn if the signature uses the
l
attribute to limit whch part of the body is included in the signature and there are non-white-space data after the signed body. - authentication_results
-
returns a line usable in Authentication-Results header
- result
-
Will return the latest computed result, i.e. like
next
. - authentication_results
-
Will return a string which can be used for the
Authentication-Results
header, see RFC 7601. - filter($sub)
-
Sets a filter function and applies it immediately if the mail header is already known. See
filter
argument ofnew
for more details.
Apart from these methods the following utility functions are provided
- parse_signature($dkim_sig,\$error) -> \%dkim_sig|undef
-
This parses the value from the DKIM-Signature field of mail and returns it as a hash. On any problems while interpreting the value undef will be returned and
$error
will be filled with a string representation of the problem. - parse_dkimkey($dkim_key,\$error) -> \%dkim_key|undef
-
This parses a DKIM key which is usually found as a TXT record in DNS and returns it as a hash. On any problems while interpreting the value undef will be returned and
$error
will be filled with a string representation of the problem. - parse_taglist($string,\$error) -> \%hash
-
This parses a tag list like found in DKIM record, DKIM signatures or DMARC records and returns it as a hash.
- sign($dkim_sig,$priv_key,$hdr,\$error) -> $signed_dkim_sig
-
This takes a DKIM signature
$dkim_sig
(as string or hash), an RSA private key$priv_key
(as PEM string or Crypt::OpenSSL::RSA object) and the header of the mail and computes the signature. The result$signed_dkim_sig
will be a signature string which can be put on top of the mail.If
$hdr->{l}
is defined and0
then the signature will contain an 'l' attribute with the full length of the body.If
$hdr->{h_auto}
is true it will determine the necessary minimal protection needed for the headers, i.e. critical headers will be included in theh
attribute one more time than they are set to protect against an additional definition. To achieve a secure by default behavior$hdr->{h_auto}
is true by default and need to be explicitly set to false to achieve potential insecure behavior.if
$hdr->{h}
is set any headers in$hdr->{h}
which are not yet in theh
attribute due to$hdr->{h_auto}
will be added also.On errors $error will be set and undef will returned.
SECURITY
The protection offered by DKIM can be easily be weakened by using insufficient header protection in the h
attribute of the signature of by using the l
attribute and having data which are not covered by the body hash.
Mail::DKIM::Iterator
will warn if it detects insufficent protection inside the DKIM signature, i.e. if critical headers are not signed or if the body has non-white-space data not covered by the body hash. Check the warning
function on the result to get these warnings. As critical are considered from, subject, content-type and content-transfer-encoding since changes to these can significantly change the interpretation of the mail by the MUA or user.
When signing Mail::DKIM::Iterator
will also protect all critical headers against modification and adding extra fields as described in RFC 6376 section 8.15. In addition to the critical headers checked when validating a signature it will also properly protect to
and cc
by default.
SEE ALSO
AUTHOR
Steffen Ullrich <sullr[at]cpan[dot]org>
COPYRIGHT
Steffen Ullrich, 2015..2019
This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.