File Coverage

blib/lib/Mail/DKIM/Verifier.pm
Criterion Covered Total %
statement 195 227 85.9
branch 63 88 71.5
condition 22 37 59.4
subroutine 21 25 84.0
pod 5 14 35.7
total 306 391 78.2


line stmt bran cond sub pod time code
1             package Mail::DKIM::Verifier;
2 5     5   383205 use strict;
  5         70  
  5         166  
3 5     5   27 use warnings;
  5         14  
  5         283  
4             our $VERSION = '1.20230212'; # 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   1941 use Mail::DKIM::Signature;
  5         63  
  5         270  
15 5     5   2231 use Mail::DKIM::DkSignature;
  5         24  
  5         155  
16 5     5   1790 use Mail::Address;
  5         9408  
  5         154  
17              
18              
19              
20 5     5   34 use base 'Mail::DKIM::Common';
  5         10  
  5         1806  
21 5     5   33 use Carp;
  5         13  
  5         9211  
22             our $MAX_SIGNATURES_TO_PROCESS = 50;
23              
24             sub init {
25 76     76 0 121 my $self = shift;
26 76         251 $self->SUPER::init;
27 76         197 $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 647     647 0 989 my $self = shift;
56 647         1190 my ( $field_name, $contents, $line ) = @_;
57              
58 647         1661 $self->SUPER::handle_header( $field_name, $contents );
59              
60 647 100       1429 if ( lc($field_name) eq 'dkim-signature' ) {
61             eval {
62 64         225 local $SIG{__DIE__};
63 64         340 my $signature = Mail::DKIM::Signature->parse($line);
64 63         234 $self->add_signature($signature);
65 63         298 1
66 64 100       155 } || 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         8 chomp( my $E = $@ );
74 1         4 $self->{signature_reject_reason} = $E;
75             };
76             }
77              
78 647 100       1630 if ( lc($field_name) eq 'domainkey-signature' ) {
79             eval {
80 17         62 local $SIG{__DIE__};
81 17         98 my $signature = Mail::DKIM::DkSignature->parse($line);
82 17         66 $self->add_signature($signature);
83 17         104 1
84 17 50       28 } || 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 80     80 0 127 my $self = shift;
99 80 50       185 croak 'wrong number of arguments' unless ( @_ == 1 );
100 80         132 my ($signature) = @_;
101              
102             # ignore signature headers once we've seen 50 or so
103             # this protects against abuse.
104 80 50       118 return if ( @{ $self->{signatures} } > $MAX_SIGNATURES_TO_PROCESS );
  80         194  
105              
106 80         119 push @{ $self->{signatures} }, $signature;
  80         189  
107              
108 80 100       183 unless ( $self->check_signature($signature) ) {
109 13         50 $signature->result( 'invalid', $self->{signature_reject_reason} );
110 13         23 return;
111             }
112              
113             # signature looks ok, go ahead and query for the public key
114 67         222 $signature->fetch_public_key;
115              
116             # create a canonicalization filter and algorithm
117 67         169 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 67         504 );
123              
124             # push through the headers parsed prior to the signature header
125 67 100       250 if ( $algorithm->wants_pre_signature_headers ) {
126              
127             # Note: this will include the signature header that led to this
128             # "algorithm"...
129 52         86 foreach my $head ( @{ $self->{headers} } ) {
  52         132  
130 67         132 $algorithm->add_header($head);
131             }
132             }
133              
134             # save the algorithm
135 67   50     174 $self->{algorithms} ||= [];
136 67         100 push @{ $self->{algorithms} }, $algorithm;
  67         183  
137             }
138              
139             sub check_signature {
140 80     80 0 113 my $self = shift;
141 80 50       173 croak 'wrong number of arguments' unless ( @_ == 1 );
142 80         146 my ($signature) = @_;
143              
144 80 100       186 unless ( $signature->check_version ) {
145              
146             # unsupported version
147 1 50       4 if ( defined $signature->version ) {
148             $self->{signature_reject_reason} =
149 1         4 '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 79 100 66     202 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         8 return 0;
166             }
167              
168 77 100       198 if ( $self->{Strict} ) {
169 4 100       11 if ( $signature->algorithm eq 'rsa-sha1' ) {
170 2         6 $self->{signature_reject_reason} = 'unsupported algorithm';
171 2 50       7 if ( defined $signature->algorithm ) {
172 2         7 $self->{signature_reject_reason} .= ' ' . $signature->algorithm;
173             }
174 2         7 return 0;
175             }
176             }
177              
178 75 100       189 unless ( $signature->check_canonicalization ) {
179              
180             # unsupported canonicalization method
181 2         9 $self->{signature_reject_reason} = 'unsupported canonicalization';
182 2 50       7 if ( defined $signature->canonicalization ) {
183             $self->{signature_reject_reason} .=
184 2         10 ' ' . $signature->canonicalization;
185             }
186 2         8 return 0;
187             }
188              
189 73 100       167 unless ( $signature->check_protocol ) {
190              
191             # unsupported query protocol
192             $self->{signature_reject_reason} =
193 4 50       11 !defined( $signature->protocol )
194             ? 'missing q tag'
195             : 'unsupported query protocol, q=' . $signature->protocol;
196 4         12 return 0;
197             }
198              
199 69 100       182 unless ( $signature->check_expiration ) {
200              
201             # signature has expired
202 2         9 $self->{signature_reject_reason} = 'signature is expired';
203 2         7 return 0;
204             }
205              
206 67 50       190 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 67 50       153 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 67 50       158 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 67         165 return 1;
228             }
229              
230             sub check_public_key {
231 56     56 0 78 my $self = shift;
232 56 50       117 croak 'wrong number of arguments' unless ( @_ == 2 );
233 56         116 my ( $signature, $public_key ) = @_;
234              
235 56         94 my $result = 0;
236             eval {
237 56         183 local $SIG{__DIE__};
238 56         100 $@ = 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 56         134 $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 54         320 my $empty_g_means_wildcard = $signature->isa('Mail::DKIM::DkSignature');
251              
252             # check public key's granularity
253 54   66     244 $result &&=
254             $public_key->check_granularity( $signature->identity,
255             $empty_g_means_wildcard );
256              
257 54 100       153 die $@ if $@;
258 49         218 1
259 56 100       80 } || do {
260 7         18 my $E = $@;
261 7         13 chomp $E;
262 7         27 $self->{signature_reject_reason} = "public key: $E";
263             };
264 56         147 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 67     67 0 139 my ($signature) = @_;
272              
273 67         147 my $d = $signature->domain;
274 67         174 my $i = $signature->identity;
275 67 50 33     489 if ( defined($i) && $i =~ /\@([^@]*)$/ ) {
276 67         163 return match_subdomain( $1, $d );
277             }
278 0         0 return 0;
279             }
280              
281             sub match_subdomain {
282 67 50   67 0 151 croak 'wrong number of arguments' unless ( @_ == 2 );
283 67         199 my ( $subdomain, $superdomain ) = @_;
284              
285 67         189 my $tmp = substr( ".$subdomain", -1 - length($superdomain) );
286 67         254 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 75     75 0 133 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 75 100 66     107 if ( @{ $self->{signatures} } == 0
  75         223  
300             && !defined( $self->{signature_reject_reason} ) )
301             {
302 1         5 $self->{result} = 'none';
303 1         3 return;
304             }
305              
306 74         115 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  74         158  
307 67         186 $algorithm->finish_header( Headers => $self->{headers} );
308             }
309              
310             # stop processing signatures that are already known to be invalid
311 74         175 @{ $self->{algorithms} } = grep {
312 67         194 my $sig = $_->signature;
313 67   33     179 !( $sig->result && $sig->result eq 'invalid' );
314 74         134 } @{ $self->{algorithms} };
  74         167  
315              
316 74 100 66     113 if ( @{ $self->{algorithms} } == 0
  74         264  
317 12         50 && @{ $self->{signatures} } > 0 )
318             {
319 12   50     40 $self->{result} = $self->{signatures}->[0]->result || 'invalid';
320             $self->{details} = $self->{signatures}->[0]->{verify_details}
321 12   33     57 || $self->{signature_reject_reason};
322 12         26 return;
323             }
324             }
325              
326             sub _check_and_verify_signature {
327 67     67   106 my $self = shift;
328 67         125 my ($algorithm) = @_;
329              
330             # check signature
331 67         148 my $signature = $algorithm->signature;
332 67 100       147 unless ( check_signature_identity($signature) ) {
333 3         13 $self->{signature_reject_reason} = 'bad identity';
334 3         13 return ( 'invalid', $self->{signature_reject_reason} );
335             }
336              
337             # get public key
338 64         124 my $pkey;
339 64         173 eval { $pkey = $signature->get_public_key; 1 }
  56         132  
340 64 100       107 || do {
341 8         21 my $E = $@;
342 8         17 chomp $E;
343 8         30 $self->{signature_reject_reason} = "public key: $E";
344 8         29 return ( 'invalid', $self->{signature_reject_reason} );
345             };
346              
347 56 100       167 unless ( $self->check_public_key( $signature, $pkey ) ) {
348 7         26 return ( 'invalid', $self->{signature_reject_reason} );
349             }
350              
351             # make sure key is big enough
352 49         160 my $keysize = $pkey->cork->size * 8; # in bits
353 49 100 100     171 if ( $keysize < 1024 && $self->{Strict} ) {
354 1         5 $self->{signature_reject_reason} = "Key length $keysize too short";
355 1         4 return ( 'fail', $self->{signature_reject_reason} );
356             }
357              
358             # verify signature
359 48         85 my $result;
360             my $details;
361 48         77 local $@ = undef;
362             eval {
363 48 100       197 $result = $algorithm->verify() ? 'pass' : 'fail';
364 46   100     182 $details = $algorithm->{verification_details} || $@;
365 46         116 1
366 48 100       85 } || do {
367              
368             # see also add_signature
369 2         10 chomp( my $E = $@ );
370 2 50       22 if ( $E =~ /(OpenSSL error: .*?) at / ) {
    0          
371 2         6 $E = $1;
372             }
373             elsif ( $E =~ /^(panic:.*?) at / ) {
374 0         0 $E = "OpenSSL $1";
375             }
376 2         3 $result = 'fail';
377 2         5 $details = $E;
378             };
379 48         153 return ( $result, $details );
380             }
381              
382             sub finish_body {
383 75     75 0 162 my $self = shift;
384              
385 75         103 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  75         164  
386              
387             # finish canonicalizing
388 67         211 $algorithm->finish_body;
389              
390 67         177 my ( $result, $details ) =
391             $self->_check_and_verify_signature($algorithm);
392              
393             # save the results of this signature verification
394 67         183 $algorithm->{result} = $result;
395 67         162 $algorithm->{details} = $details;
396 67         187 $algorithm->signature->result( $result, $details );
397              
398             # collate results ... ignore failed signatures if we already got
399             # one to pass
400 67 100 100     235 if ( !$self->{result} || $result eq 'pass' ) {
401 64         176 $self->{signature} = $algorithm->signature;
402 64         133 $self->{result} = $result;
403 64         242 $self->{details} = $details;
404             }
405             }
406             }
407              
408              
409             sub fetch_author_domain_policies {
410 0     0 1 0 my $self = shift;
411 5     5   2306 use Mail::DKIM::AuthorDomainPolicy;
  5         15  
  5         615  
412              
413 0 0       0 return () unless $self->{headers_by_name}->{from};
414 0         0 my @list = Mail::Address->parse( $self->{headers_by_name}->{from} );
415 0         0 my @authors = map { $_->address } @list;
  0         0  
416              
417             # fetch the policies
418             return map {
419 0         0 Mail::DKIM::AuthorDomainPolicy->fetch(
  0         0  
420             Protocol => 'dns',
421             Author => $_,
422             )
423             } @authors;
424             }
425              
426              
427             sub fetch_author_policy {
428 0     0 1 0 my $self = shift;
429 0         0 my ($author) = @_;
430 5     5   2278 use Mail::DKIM::DkimPolicy;
  5         11  
  5         350  
431              
432             # determine address found in the "From"
433 0   0     0 $author ||= $self->message_originator->address;
434              
435             # fetch the policy
436 0         0 return Mail::DKIM::DkimPolicy->fetch(
437             Protocol => 'dns',
438             Author => $author,
439             );
440             }
441              
442              
443             sub fetch_sender_policy {
444 0     0 1 0 my $self = shift;
445 5     5   2143 use Mail::DKIM::DkPolicy;
  5         15  
  5         841  
446              
447             # determine addresses found in the "From" and "Sender" headers
448 0         0 my $author = $self->message_originator->address;
449 0         0 my $sender = $self->message_sender->address;
450              
451             # fetch the policy
452 0         0 return Mail::DKIM::DkPolicy->fetch(
453             Protocol => 'dns',
454             Author => $author,
455             Sender => $sender,
456             );
457             }
458              
459              
460             sub policies {
461 0     0 1 0 my $self = shift;
462              
463 0         0 my $sender_policy = eval { $self->fetch_sender_policy() };
  0         0  
464 0         0 my $author_policy = eval { $self->fetch_author_policy() };
  0         0  
465             return (
466 0 0       0 $sender_policy ? $sender_policy : (),
    0          
467             $author_policy ? $author_policy : (),
468             $self->fetch_author_domain_policies(),
469             );
470             }
471              
472              
473              
474              
475             sub signatures {
476 4     4 1 959 my $self = shift;
477 4 50       16 croak 'unexpected argument' if @_;
478              
479 4         9 return @{ $self->{signatures} };
  4         16  
480             }
481              
482             1;
483              
484             __END__