File Coverage

blib/lib/Mail/DKIM/Verifier.pm
Criterion Covered Total %
statement 197 230 85.6
branch 65 92 70.6
condition 24 39 61.5
subroutine 21 25 84.0
pod 5 14 35.7
total 312 400 78.0


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