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 if sign 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:

signature - the DKIM-Signature value, only if DKIM_PASS

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 of new 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 and 0 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 the h 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 the h 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

Mail::DKIM

Mail::SPF::Iterator

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.