File Coverage

blib/lib/Mail/DKIM/Verifier.pm
Criterion Covered Total %
statement 199 232 85.7
branch 67 94 71.2
condition 24 39 61.5
subroutine 21 25 84.0
pod 5 14 35.7
total 316 404 78.2


line stmt bran cond sub pod time code
1             package Mail::DKIM::Verifier;
2 6     6   657596 use strict;
  6         14  
  6         273  
3 6     6   37 use warnings;
  6         13  
  6         625  
4             our $VERSION = '1.20240923'; # VERSION
5             # ABSTRACT: verifies a DKIM-signed message
6              
7             # Copyright 2005-2009 Messiah College. All rights reserved.
8             # Jason Long
9              
10             # Copyright (c) 2004 Anthony D. Urso. All rights reserved.
11             # This program is free software; you can redistribute it and/or
12             # modify it under the same terms as Perl itself.
13              
14 6     6   2635 use Mail::DKIM::Signature;
  6         36  
  6         388  
15 6     6   2954 use Mail::DKIM::DkSignature;
  6         23  
  6         230  
16 6     6   1878 use Mail::Address;
  6         12187  
  6         242  
17              
18              
19              
20 6     6   78 use base 'Mail::DKIM::Common';
  6         26  
  6         2358  
21 6     6   43 use Carp;
  6         15  
  6         14008  
22             our $MAX_SIGNATURES_TO_PROCESS = 50;
23              
24             sub init {
25 84     84 0 283 my $self = shift;
26 84         435 $self->SUPER::init;
27 84         408 $self->{signatures} = [];
28             }
29              
30             # @{$dkim->{signatures}}
31             # array of L objects, representing all
32             # parseable signatures found in the header,
33             # ordered from the top of the header to the bottom.
34             #
35             # $dkim->{signature_reject_reason}
36             # simple string listing a reason, if any, for not using a signature.
37             # This may be a helpful diagnostic if there is a signature in the header,
38             # but was found not to be valid. It will be ambiguous if there are more
39             # than one signatures that could not be used.
40             #
41             # $dkim->{signature}
42             # the L selected as the "best" signature.
43             #
44             # @{$dkim->{headers}}
45             # array of strings, each member is one header, in its original format.
46             #
47             # $dkim->{algorithms}
48             # array of algorithms, one for each signature being verified.
49             #
50             # $dkim->{result}
51             # string; the result of the verification (see the result() method)
52             #
53              
54             sub handle_header {
55 709     709 0 1231 my $self = shift;
56 709         1769 my ( $field_name, $contents, $line ) = @_;
57              
58 709         2237 $self->SUPER::handle_header( $field_name, $contents );
59              
60 709 100       2014 if ( lc($field_name) eq 'dkim-signature' ) {
61             eval {
62 72         358 local $SIG{__DIE__};
63 72         619 my $signature = Mail::DKIM::Signature->parse($line);
64 71         334 $self->add_signature($signature);
65 71         471 1
66 72 100       158 } || do {
67              
68             # the only reason an error should be thrown is if the
69             # signature really is unparse-able
70              
71             # otherwise, invalid signatures are caught in finish_header()
72              
73 1         4 chomp( my $E = $@ );
74 1         4 $self->{signature_reject_reason} = $E;
75             };
76             }
77              
78 709 100       2395 if ( lc($field_name) eq 'domainkey-signature' ) {
79             eval {
80 17         87 local $SIG{__DIE__};
81 17         204 my $signature = Mail::DKIM::DkSignature->parse($line);
82 17         87 $self->add_signature($signature);
83 17         150 1
84 17 50       37 } || do {
85              
86             # the only reason an error should be thrown is if the
87             # signature really is unparse-able
88              
89             # otherwise, invalid signatures are caught in finish_header()
90              
91 0         0 chomp( my $E = $@ );
92 0         0 $self->{signature_reject_reason} = $E;
93             };
94             }
95             }
96              
97             sub add_signature {
98 88     88 0 191 my $self = shift;
99 88 50       254 croak 'wrong number of arguments' unless ( @_ == 1 );
100 88         214 my ($signature) = @_;
101              
102             # Set verification time if we have one
103 88 100       308 if ($self->{verify_time}) {
104 1         5 $signature->set_verify_time($self->{verify_time});
105             }
106              
107             # ignore signature headers once we've seen 50 or so
108             # this protects against abuse.
109 88 50       179 return if ( @{ $self->{signatures} } > $MAX_SIGNATURES_TO_PROCESS );
  88         316  
110              
111 88         184 push @{ $self->{signatures} }, $signature;
  88         236  
112              
113 88 100       317 unless ( $self->check_signature($signature) ) {
114 14         65 $signature->result( 'invalid', $self->{signature_reject_reason} );
115 14         33 return;
116             }
117              
118             # signature looks ok, go ahead and query for the public key
119 74         307 $signature->fetch_public_key;
120              
121             # create a canonicalization filter and algorithm
122 74         284 my $algorithm_class =
123             $signature->get_algorithm_class( $signature->algorithm );
124             my $algorithm = $algorithm_class->new(
125             Signature => $signature,
126             Debug_Canonicalization => $self->{Debug_Canonicalization},
127 74         945 );
128              
129             # push through the headers parsed prior to the signature header
130 74 100       375 if ( $algorithm->wants_pre_signature_headers ) {
131              
132             # Note: this will include the signature header that led to this
133             # "algorithm"...
134 59         118 foreach my $head ( @{ $self->{headers} } ) {
  59         223  
135 67         191 $algorithm->add_header($head);
136             }
137             }
138              
139             # save the algorithm
140 74   50     293 $self->{algorithms} ||= [];
141 74         159 push @{ $self->{algorithms} }, $algorithm;
  74         290  
142             }
143              
144             sub check_signature {
145 88     88 0 161 my $self = shift;
146 88 50       336 croak 'wrong number of arguments' unless ( @_ == 1 );
147 88         223 my ($signature) = @_;
148              
149 88 100       369 unless ( $signature->check_version ) {
150              
151             # unsupported version
152 1 50       4 if ( defined $signature->version ) {
153             $self->{signature_reject_reason} =
154 1         4 'unsupported version ' . $signature->version;
155             }
156             else {
157 0         0 $self->{signature_reject_reason} = 'missing v tag';
158             }
159 1         6 return 0;
160             }
161              
162 87 100 66     262 unless ( $signature->algorithm
163             && $signature->get_algorithm_class( $signature->algorithm ) )
164             {
165             # unsupported algorithm
166 2         7 $self->{signature_reject_reason} = 'unsupported algorithm';
167 2 50       6 if ( defined $signature->algorithm ) {
168 2         7 $self->{signature_reject_reason} .= ' ' . $signature->algorithm;
169             }
170 2         9 return 0;
171             }
172              
173 85 100       346 if ( $self->{Strict} ) {
174 4 100       124 if ( $signature->algorithm eq 'rsa-sha1' ) {
175 2         6 $self->{signature_reject_reason} = 'unsupported algorithm';
176 2 50       6 if ( defined $signature->algorithm ) {
177 2         8 $self->{signature_reject_reason} .= ' ' . $signature->algorithm;
178             }
179 2         9 return 0;
180             }
181             }
182              
183 83 100       327 unless ( $signature->check_canonicalization ) {
184              
185             # unsupported canonicalization method
186 2         7 $self->{signature_reject_reason} = 'unsupported canonicalization';
187 2 50       7 if ( defined $signature->canonicalization ) {
188             $self->{signature_reject_reason} .=
189 2         10 ' ' . $signature->canonicalization;
190             }
191 2         10 return 0;
192             }
193              
194 81 100       276 unless ( $signature->check_protocol ) {
195              
196             # unsupported query protocol
197             $self->{signature_reject_reason} =
198 4 50       13 !defined( $signature->protocol )
199             ? 'missing q tag'
200             : 'unsupported query protocol, q=' . $signature->protocol;
201 4         16 return 0;
202             }
203              
204 77 100       348 unless ( $signature->check_expiration ) {
205              
206             # signature has expired
207 3         10 $self->{signature_reject_reason} = 'signature is expired';
208 3         11 return 0;
209             }
210              
211 74 50       241 unless ( defined $signature->domain ) {
212              
213             # no domain specified
214 0         0 $self->{signature_reject_reason} = 'missing d tag';
215 0         0 return 0;
216             }
217              
218 74 50       288 if ( $signature->domain eq '' ) {
219              
220             # blank domain
221 0         0 $self->{signature_reject_reason} = 'invalid domain in d tag';
222 0         0 return 0;
223             }
224              
225 74 50       236 unless ( defined $signature->selector ) {
226              
227             # no selector specified
228 0         0 $self->{signature_reject_reason} = 'missing s tag';
229 0         0 return 0;
230             }
231              
232 74         239 return 1;
233             }
234              
235             sub check_public_key {
236 60     60 0 117 my $self = shift;
237 60 50       192 croak 'wrong number of arguments' unless ( @_ == 2 );
238 60         155 my ( $signature, $public_key ) = @_;
239              
240 60         114 my $result = 0;
241             eval {
242 60         272 local $SIG{__DIE__};
243 60         126 $@ = undef;
244              
245             # HACK- I'm indecisive here about whether I want the
246             # check_foo functions to return false or to "die"
247             # on failure
248              
249             # check public key's allowed hash algorithms
250 60         294 $result =
251             $public_key->check_hash_algorithm( $signature->hash_algorithm );
252              
253             # HACK- DomainKeys signatures are allowed to have an empty g=
254             # tag in the public key
255 58         609 my $empty_g_means_wildcard = $signature->isa('Mail::DKIM::DkSignature');
256              
257             # check public key's granularity
258 58   66     365 $result &&=
259             $public_key->check_granularity( $signature->identity,
260             $empty_g_means_wildcard );
261              
262 58 100       197 die $@ if $@;
263 53         286 1
264 60 100       117 } || do {
265 7         16 my $E = $@;
266 7         17 chomp $E;
267 7         24 $self->{signature_reject_reason} = "public key: $E";
268             };
269 60         220 return $result;
270             }
271              
272             # returns true if the i= tag is an address with a domain matching or
273             # a subdomain of the d= tag
274             #
275             sub check_signature_identity {
276 74     74 0 162 my ($signature) = @_;
277              
278 74         258 my $d = $signature->domain;
279 74         304 my $i = $signature->identity;
280 74 50 33     784 if ( defined($i) && $i =~ /\@([^@]*)$/ ) {
281 74         233 return match_subdomain( $1, $d );
282             }
283 0         0 return 0;
284             }
285              
286             sub match_subdomain {
287 74 50   74 0 291 croak 'wrong number of arguments' unless ( @_ == 2 );
288 74         342 my ( $subdomain, $superdomain ) = @_;
289              
290 74         347 my $tmp = substr( ".$subdomain", -1 - length($superdomain) );
291 74         409 return ( lc ".$superdomain" eq lc $tmp );
292             }
293              
294             #
295             # called when the verifier has received the last of the message headers
296             # (body is still to come)
297             #
298             sub finish_header {
299 83     83 0 168 my $self = shift;
300              
301             # Signatures we found and were successfully parsed are stored in
302             # $self->{signatures}. If none were found, our result is "none".
303              
304 83 100 66     153 if ( @{ $self->{signatures} } == 0
  83         439  
305             && !defined( $self->{signature_reject_reason} ) )
306             {
307 1         4 $self->{result} = 'none';
308 1         21 return;
309             }
310              
311 82         151 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  82         241  
312 74         381 $algorithm->finish_header( Headers => $self->{headers} );
313             }
314              
315             # stop processing signatures that are already known to be invalid
316 82         262 @{ $self->{algorithms} } = grep {
317 74         315 my $sig = $_->signature;
318 74   33     321 !( $sig->result && $sig->result eq 'invalid' );
319 82         189 } @{ $self->{algorithms} };
  82         356  
320              
321 82 100 66     148 if ( @{ $self->{algorithms} } == 0
  82         428  
322 13         62 && @{ $self->{signatures} } > 0 )
323             {
324 13   50     53 $self->{result} = $self->{signatures}->[0]->result || 'invalid';
325             $self->{details} = $self->{signatures}->[0]->{verify_details}
326 13   33     74 || $self->{signature_reject_reason};
327 13         33 return;
328             }
329             }
330              
331             sub _check_and_verify_signature {
332 74     74   174 my $self = shift;
333 74         189 my ($algorithm) = @_;
334              
335             # check signature
336 74         250 my $signature = $algorithm->signature;
337 74 100       256 unless ( check_signature_identity($signature) ) {
338 3         13 $self->{signature_reject_reason} = 'bad identity';
339 3         15 return ( 'invalid', $self->{signature_reject_reason} );
340             }
341              
342             # get public key
343 71         138 my $pkey;
344 71         296 eval { $pkey = $signature->get_public_key; 1 }
  60         211  
345 71 100       163 || do {
346 11         30 my $E = $@;
347 11         33 chomp $E;
348 11         35 $self->{signature_reject_reason} = "public key: $E";
349 11         62 return ( 'invalid', $self->{signature_reject_reason} );
350             };
351              
352 60 100       269 unless ( $self->check_public_key( $signature, $pkey ) ) {
353 7         31 return ( 'invalid', $self->{signature_reject_reason} );
354             }
355              
356             # special handling for RSA signatures
357 53   100     206 my $k = $pkey->get_tag('k') || 'rsa';
358 53 100       180 if ($k eq 'rsa') {
359             # make sure key is big enough
360 50         215 my $keysize = $pkey->cork->size * 8; # in bits
361 50 100 100     281 if ( $keysize < 1024 && $self->{Strict} ) {
362 1         34 $self->{signature_reject_reason} = "Key length $keysize too short";
363 1         8 return ( 'fail', $self->{signature_reject_reason} );
364             }
365             }
366              
367             # verify signature
368 52         169 my $result;
369             my $details;
370 52         113 local $@ = undef;
371             eval {
372 52 100       278 $result = $algorithm->verify() ? 'pass' : 'fail';
373 50   100     323 $details = $algorithm->{verification_details} || $@;
374 50         187 1
375 52 100       132 } || do {
376              
377             # see also add_signature
378 2         15 chomp( my $E = $@ );
379 2 50       24 if ( $E =~ /(OpenSSL error: .*?) at / ) {
    0          
    0          
380 2         9 $E = $1;
381             }
382             elsif ( $E =~ /^(panic:.*?) at / ) {
383 0         0 $E = "OpenSSL $1";
384             }
385             elsif ( $E =~ /^FATAL: (.*) at / ) {
386 0         0 $E = "Ed25519 $1";
387             }
388 2         5 $result = 'fail';
389 2         14 $details = $E;
390             };
391 52         306 return ( $result, $details );
392             }
393              
394             sub finish_body {
395 83     83 0 170 my $self = shift;
396              
397 83         177 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  83         236  
398              
399             # finish canonicalizing
400 74         360 $algorithm->finish_body;
401              
402 74         339 my ( $result, $details ) =
403             $self->_check_and_verify_signature($algorithm);
404              
405             # save the results of this signature verification
406 74         254 $algorithm->{result} = $result;
407 74         226 $algorithm->{details} = $details;
408 74         331 $algorithm->signature->result( $result, $details );
409              
410             # collate results ... ignore failed signatures if we already got
411             # one to pass
412 74 100 100     365 if ( !$self->{result} || $result eq 'pass' ) {
413 71         210 $self->{signature} = $algorithm->signature;
414 71         182 $self->{result} = $result;
415 71         362 $self->{details} = $details;
416             }
417             }
418             }
419              
420              
421             sub fetch_author_domain_policies {
422 0     0 1 0 my $self = shift;
423 6     6   4622 use Mail::DKIM::AuthorDomainPolicy;
  6         20  
  6         929  
424              
425 0 0       0 return () unless $self->{headers_by_name}->{from};
426 0         0 my @list = Mail::Address->parse( $self->{headers_by_name}->{from} );
427 0         0 my @authors = map { $_->address } @list;
  0         0  
428              
429             # fetch the policies
430             return map {
431 0         0 Mail::DKIM::AuthorDomainPolicy->fetch(
  0         0  
432             Protocol => 'dns',
433             Author => $_,
434             )
435             } @authors;
436             }
437              
438              
439             sub fetch_author_policy {
440 0     0 1 0 my $self = shift;
441 0         0 my ($author) = @_;
442 6     6   5057 use Mail::DKIM::DkimPolicy;
  6         25  
  6         519  
443              
444             # determine address found in the "From"
445 0   0     0 $author ||= $self->message_originator->address;
446              
447             # fetch the policy
448 0         0 return Mail::DKIM::DkimPolicy->fetch(
449             Protocol => 'dns',
450             Author => $author,
451             );
452             }
453              
454              
455             sub fetch_sender_policy {
456 0     0 1 0 my $self = shift;
457 6     6   3008 use Mail::DKIM::DkPolicy;
  6         20  
  6         1189  
458              
459             # determine addresses found in the "From" and "Sender" headers
460 0         0 my $author = $self->message_originator->address;
461 0         0 my $sender = $self->message_sender->address;
462              
463             # fetch the policy
464 0         0 return Mail::DKIM::DkPolicy->fetch(
465             Protocol => 'dns',
466             Author => $author,
467             Sender => $sender,
468             );
469             }
470              
471              
472             sub policies {
473 0     0 1 0 my $self = shift;
474              
475 0         0 my $sender_policy = eval { $self->fetch_sender_policy() };
  0         0  
476 0         0 my $author_policy = eval { $self->fetch_author_policy() };
  0         0  
477             return (
478 0 0       0 $sender_policy ? $sender_policy : (),
    0          
479             $author_policy ? $author_policy : (),
480             $self->fetch_author_domain_policies(),
481             );
482             }
483              
484              
485              
486              
487             sub signatures {
488 4     4 1 3554 my $self = shift;
489 4 50       24 croak 'unexpected argument' if @_;
490              
491 4         10 return @{ $self->{signatures} };
  4         22  
492             }
493              
494             1;
495              
496             __END__