File Coverage

blib/lib/Mail/DKIM/Signer.pm
Criterion Covered Total %
statement 153 176 86.9
branch 59 84 70.2
condition 13 28 46.4
subroutine 19 21 90.4
pod 10 15 66.6
total 254 324 78.4


line stmt bran cond sub pod time code
1             package Mail::DKIM::Signer;
2 6     6   723577 use strict;
  6         27  
  6         259  
3 6     6   33 use warnings;
  6         62  
  6         553  
4             our $VERSION = '1.20240923'; # VERSION
5             # ABSTRACT: generates a DKIM signature for a message
6              
7             # Copyright 2005-2007 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   2748 use Mail::DKIM::PrivateKey;
  6         58  
  6         289  
15 6     6   3903 use Mail::DKIM::Signature;
  6         123  
  6         806  
16              
17              
18 6     6   66 use base 'Mail::DKIM::Common';
  6         14  
  6         6943  
19 6     6   54 use Carp;
  6         12  
  6         16708  
20              
21             # PROPERTIES
22             #
23             # public:
24             #
25             # $dkim->{Algorithm}
26             # identifies what algorithm to use when signing the message
27             # default is "rsa-sha1"
28             #
29             # $dkim->{Domain}
30             # identifies what domain the message is signed for
31             #
32             # $dkim->{KeyFile}
33             # name of the file containing the private key used to sign
34             #
35             # $dkim->{Method}
36             # identifies what canonicalization method to use when signing
37             # the message. default is "relaxed"
38             #
39             # $dkim->{Policy}
40             # a signing policy (of type Mail::DKIM::SigningPolicy)
41             #
42             # $dkim->{Selector}
43             # identifies name of the selector identifying the key
44             #
45             # $dkim->{Key}
46             # the loaded private key
47             #
48             # private:
49             #
50             # $dkim->{algorithms} = []
51             # an array of algorithm objects... an algorithm object is created for
52             # each signature being added to the message
53             #
54             # $dkim->{result}
55             # result of the signing policy: "signed" or "skipped"
56             #
57             # $dkim->{signature}
58             # the created signature (of type Mail::DKIM::Signature)
59              
60             sub init {
61 25     25 0 47 my $self = shift;
62 25         166 $self->SUPER::init;
63              
64 25 100       96 unless ( $self->{'Algorithm'} ) {
65              
66             # use default algorithm
67 10         27 $self->{'Algorithm'} = 'rsa-sha1';
68             }
69              
70 25         50 my $type = 'rsa'; # default
71 25 100       169 $type = 'ed25519' if ( $self->{'Algorithm'} =~ /^ed25519/ );
72              
73 25 100       103 if ( defined $self->{KeyFile} ) {
74             $self->{Key} ||=
75             Mail::DKIM::PrivateKey->load( File => $self->{KeyFile},
76 23   33     300 Type => $type );
77             }
78              
79 23 100       109 unless ( $self->{'Method'} ) {
80              
81             # use default canonicalization method
82 10         28 $self->{'Method'} = 'relaxed';
83             }
84 23 100       73 unless ( $self->{'Domain'} ) {
85              
86             # use default domain
87 10         24 $self->{'Domain'} = 'example.org';
88             }
89 23 100       107 unless ( $self->{'Selector'} ) {
90              
91             # use default selector
92 10         37 $self->{'Selector'} = 'unknown';
93             }
94              
95             }
96              
97             sub finish_header {
98 23     23 0 57 my $self = shift;
99              
100 23         71 $self->{algorithms} = [];
101              
102 23         53 my $policy = $self->{Policy};
103 23 100 66     150 if ( UNIVERSAL::isa( $policy, 'CODE' ) ) {
    100          
104              
105             # policy is a subroutine ref
106 8         28 my $default_sig = $policy->($self);
107 8 100 100     15 unless ( @{ $self->{algorithms} } || $default_sig ) {
  8         37  
108 1         2 $self->{'result'} = 'skipped';
109 1         2 return;
110             }
111             }
112             elsif ( $policy && $policy->can('apply') ) {
113              
114             # policy is a Perl object or class
115 2         7 my $default_sig = $policy->apply($self);
116 2 50 33     5 unless ( @{ $self->{algorithms} } || $default_sig ) {
  2         9  
117 0         0 $self->{'result'} = 'skipped';
118 0         0 return;
119             }
120             }
121              
122 22 100       39 unless ( @{ $self->{algorithms} } ) {
  22         75  
123              
124             # no algorithms were created yet, so construct a signature
125             # using the current signature properties
126              
127             # check properties
128 16 50       50 unless ( $self->{'Algorithm'} ) {
129 0         0 die 'invalid algorithm property';
130             }
131 16 50       82 unless ( $self->{'Method'} ) {
132 0         0 die 'invalid method property';
133             }
134 16 50       47 unless ( $self->{'Domain'} ) {
135 0         0 die 'invalid domain property';
136             }
137 16 50       49 unless ( $self->{'Selector'} ) {
138 0         0 die 'invalid selector property';
139             }
140              
141             $self->add_signature(
142             Mail::DKIM::Signature->new(
143             Algorithm => $self->{'Algorithm'},
144             Method => $self->{'Method'},
145             Headers => $self->headers,
146             Domain => $self->{'Domain'},
147             Selector => $self->{'Selector'},
148             Key => $self->{'Key'},
149             KeyFile => $self->{'KeyFile'},
150             (
151             $self->{'Identity'} ? ( Identity => $self->{'Identity'} )
152             : ()
153             ),
154             (
155             $self->{'Timestamp'} ? ( Timestamp => $self->{'Timestamp'} )
156             : ()
157             ),
158             (
159             $self->{'Expiration'} ? ( Expiration => $self->{'Expiration'} )
160             : ()
161             ),
162             (
163 16 100       86 $self->{'Tags'} ? ( Tags => $self->{'Tags'} )
    100          
    100          
    50          
164             : ()
165             ),
166             )
167             );
168             }
169              
170 22         42 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  22         69  
171              
172             # output header as received so far into canonicalization
173 23         54 foreach my $header ( @{ $self->{headers} } ) {
  23         58  
174 70         194 $algorithm->add_header($header);
175             }
176 23         98 $algorithm->finish_header( Headers => $self->{headers} );
177             }
178             }
179              
180             sub finish_body {
181 23     23 0 37 my $self = shift;
182              
183 23         36 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  23         65  
184              
185             # finished canonicalizing
186 23         123 $algorithm->finish_body;
187              
188 23         51 my $type = 'rsa'; # default
189 23 100       90 $type = 'ed25519' if ( $self->{'Algorithm'} =~ /^ed25519/ );
190              
191             # load the private key file if necessary
192 23         114 my $signature = $algorithm->signature;
193             my $key =
194             $signature->{Key}
195             || $signature->{KeyFile}
196             || $self->{Key}
197 23   66     104 || $self->{KeyFile};
198 23 100 66     1555 if ( defined($key) && !ref($key) ) {
199 1         7 $key = Mail::DKIM::PrivateKey->load( File => $key,
200             Type => $type );
201             }
202             $key
203 23 50       93 or die "no key available to sign with\n";
204              
205             # compute signature value
206 23         91 my $signb64 = $algorithm->sign($key);
207 23         139 $signature->data($signb64);
208              
209             # insert linebreaks in signature data, if desired
210 23         114 $signature->prettify_safe();
211              
212 23         89 $self->{signature} = $signature;
213 23         121 $self->{result} = 'signed';
214             }
215             }
216              
217              
218             sub add_signature {
219 23     23 1 39 my $self = shift;
220 23         77 my $signature = shift;
221              
222             # create a canonicalization filter and algorithm
223 23 50 0     63 my $algorithm_class =
224             $signature->get_algorithm_class( $signature->algorithm )
225             or die 'unsupported algorithm ' . ( $signature->algorithm || '' ) . "\n";
226             my $algorithm = $algorithm_class->new(
227             Signature => $signature,
228             Debug_Canonicalization => $self->{Debug_Canonicalization},
229 23         441 );
230 23         57 push @{ $self->{algorithms} }, $algorithm;
  23         72  
231 23         83 return;
232             }
233              
234              
235             sub algorithm {
236 3     3 1 9 my $self = shift;
237 3 50       19 if ( @_ == 1 ) {
238 3         10 $self->{Algorithm} = shift;
239             }
240 3         4 return $self->{Algorithm};
241             }
242              
243              
244             sub domain {
245 3     3 1 14 my $self = shift;
246 3 50       6 if ( @_ == 1 ) {
247 3         6 $self->{Domain} = shift;
248             }
249 3         3 return $self->{Domain};
250             }
251              
252              
253              
254             # these are headers that "should" be included in the signature,
255             # according to the DKIM spec.
256             my @DEFAULT_HEADERS = qw(From Sender Reply-To Subject Date
257             Message-ID To Cc MIME-Version
258             Content-Type Content-Transfer-Encoding Content-ID Content-Description
259             Resent-Date Resent-From Resent-Sender Resent-To Resent-cc
260             Resent-Message-ID
261             In-Reply-To References
262             List-Id List-Help List-Unsubscribe List-Subscribe
263             List-Post List-Owner List-Archive);
264              
265             sub process_headers_hash {
266 1     1 0 2 my $self = shift;
267              
268 1         2 my @headers;
269              
270             # these are the header fields we found in the message we're signing
271 1         2 my @found_headers = @{ $self->{header_field_names} };
  1         5  
272              
273             # Convert all keys to lower case
274 1         2 foreach my $header ( keys %{ $self->{'ExtendedHeaders'} } ) {
  1         4  
275 4 50       11 next if $header eq lc $header;
276 4 50       7 if ( exists $self->{'ExtendedHeaders'}->{ lc $header } ) {
277              
278             # Merge
279 0         0 my $first = $self->{'ExtendedHeaders'}->{ lc $header };
280 0         0 my $second = $self->{'ExtendedHeaders'}->{$header};
281 0 0 0     0 if ( $first eq '+' || $second eq '+' ) {
    0 0        
282 0         0 $self->{'ExtendedHeaders'}->{ lc $header } = '+';
283             }
284             elsif ( $first eq '*' || $second eq '*' ) {
285 0         0 $self->{'ExtendedHeaders'}->{ lc $header } = '*';
286             }
287             else {
288 0         0 $self->{'ExtendedHeaders'}->{ lc $header } = $first + $second;
289             }
290             }
291             else {
292             # Rename
293             $self->{'ExtendedHeaders'}->{ lc $header } =
294 4         8 $self->{'ExtendedHeaders'}->{$header};
295             }
296 4         5 delete $self->{'ExtendedHeaders'}->{$header};
297             }
298              
299             # Add the default headers
300 1         4 foreach my $default (@DEFAULT_HEADERS) {
301 28 100       45 if ( !exists $self->{'ExtendedHeaders'}->{ lc $default } ) {
302 26         46 $self->{'ExtendedHeaders'}->{ lc $default } = '*';
303             }
304             }
305              
306             # Build a count of found headers
307 1         3 my $header_counts = {};
308 1         1 foreach my $header (@found_headers) {
309 10 100       14 if ( !exists $header_counts->{ lc $header } ) {
310 7         13 $header_counts->{ lc $header } = 1;
311             }
312             else {
313 3         6 $header_counts->{ lc $header } = $header_counts->{ lc $header } + 1;
314             }
315             }
316              
317 1         2 foreach my $header ( sort keys %{ $self->{'ExtendedHeaders'} } ) {
  1         12  
318 30         43 my $want_count = $self->{'ExtendedHeaders'}->{$header};
319 30   100     50 my $have_count = $header_counts->{ lc $header } || 0;
320 30         27 my $add_count = 0;
321 30 100       38 if ( $want_count eq '+' ) {
    100          
322 1         2 $add_count = $have_count + 1;
323             }
324             elsif ( $want_count eq '*' ) {
325 27         24 $add_count = $have_count;
326             }
327             else {
328 2 50       4 if ( $want_count > $have_count ) {
329 0         0 $add_count = $have_count;
330             }
331             else {
332 2         3 $add_count = $want_count;
333             }
334             }
335 30         38 for ( 1 .. $add_count ) {
336 6         8 push @headers, $header;
337             }
338             }
339 1         22 return join( ':', @headers );
340             }
341              
342             sub extended_headers {
343 1     1 1 316 my $self = shift;
344 1         3 $self->{'ExtendedHeaders'} = shift;
345 1         1 return;
346             }
347              
348             sub headers {
349 21     21 1 89 my $self = shift;
350 21 50       79 croak 'unexpected argument' if @_;
351              
352 21 100       79 if ( exists $self->{'ExtendedHeaders'} ) {
353 1         5 return $self->process_headers_hash();
354             }
355              
356             # these are the header fields we found in the message we're signing
357 20         43 my @found_headers = @{ $self->{header_field_names} };
  20         78  
358              
359             # these are the headers we actually want to sign
360 20         151 my @wanted_headers = @DEFAULT_HEADERS;
361 20 50       87 if ( $self->{Headers} ) {
362 0         0 push @wanted_headers, split /:/, $self->{Headers};
363             }
364              
365             my @headers =
366             grep {
367 20         53 my $a = $_;
  53         87  
368 53         80 scalar grep { lc($a) eq lc($_) } @wanted_headers
  1484         2368  
369             } @found_headers;
370 20         336 return join( ':', @headers );
371             }
372              
373             # return nonzero if this is header we should sign
374             sub want_header {
375 0     0 0 0 my $self = shift;
376 0         0 my ($header_name) = @_;
377              
378             #TODO- provide a way for user to specify which headers to sign
379 0         0 return scalar grep { lc($_) eq lc($header_name) } @DEFAULT_HEADERS;
  0         0  
380             }
381              
382              
383             sub key {
384 0     0 1 0 my $self = shift;
385 0 0       0 if (@_) {
386 0         0 $self->{Key} = shift;
387 0         0 $self->{KeyFile} = undef;
388             }
389 0         0 return $self->{Key};
390             }
391              
392              
393             sub key_file {
394 1     1 1 3 my $self = shift;
395 1 50       3 if (@_) {
396 1         2 $self->{Key} = undef;
397 1         2 $self->{KeyFile} = shift;
398             }
399 1         2 return $self->{KeyFile};
400             }
401              
402              
403             sub method {
404 3     3 1 10 my $self = shift;
405 3 50       7 if ( @_ == 1 ) {
406 3         4 $self->{Method} = shift;
407             }
408 3         4 return $self->{Method};
409             }
410              
411              
412              
413             sub selector {
414 3     3 1 8 my $self = shift;
415 3 50       5 if ( @_ == 1 ) {
416 3         4 $self->{Selector} = shift;
417             }
418 3         5 return $self->{Selector};
419             }
420              
421              
422             sub signatures {
423 1     1 1 2 my $self = shift;
424 1 50       3 croak 'no arguments allowed' if @_;
425 1         2 return map { $_->signature } @{ $self->{algorithms} };
  2         6  
  1         3  
426             }
427              
428              
429             1;
430              
431             __END__